diff --git a/www/apps/book/app/learn/customization/extend-features/create-links/page.mdx b/www/apps/book/app/learn/customization/extend-features/create-links/page.mdx deleted file mode 100644 index 0db80a28e0591..0000000000000 --- a/www/apps/book/app/learn/customization/extend-features/create-links/page.mdx +++ /dev/null @@ -1,115 +0,0 @@ -export const metadata = { - title: `${pageNumber} Create Links between Brand and Product Records`, -} - -# {metadata.title} - - - -This chapter covers how to create a link between the records of the `Brand` and `Product` data models as a step of the ["Extend Models" chapter](../page.mdx). - - - -## What is the Remote Link? - -The remote link is a class with utility methods to manage links between data models' records. - -It’s registered in the Medusa container under the `ContainerRegistrationKeys.REMOTE_LINK` (`remoteLink`) registration name. - -### Example: Create Link with Remote Link - -For example, consider the following step: - -export const stepHighlights = [ - ["19", "resolve", "Resolve the remote link."], - ["23", "create", "Create a link between two records."] -] - -```ts highlights={stepHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { - Modules, - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" -import { BRAND_MODULE } from "../../modules/brand" - -type LinkProductToBrandStepInput = { - productId: string - brandId: string -} - -export const linkProductToBrandStep = createStep( - "link-product-to-brand", - async ({ productId, brandId }: LinkProductToBrandStepInput, { container }) => { - const remoteLink = container.resolve( - ContainerRegistrationKeys.REMOTE_LINK - ) - - remoteLink.create({ - [Modules.PRODUCT]: { - product_id: productId, - }, - [BRAND_MODULE]: { - brand_id: brandId, - }, - }) - - return new StepResponse(undefined, { - productId, - brandId, - }) - } -) -``` - -In this step, you resolve the remote link, then use its `create` method to create a link between product and brand records. - -The `create` method accepts as a parameter an object whose properties are the names of each module, and the value is an object. - - - -Use the `Modules` enum imported from `@medusajs/framework/utils` to for the commerce module's names. - - - -The value object has a property, which is the name of the data model (as specified in `model.define`'s first parameter) followed by `_id`, and its value is the ID of the record to link. - -### Dismiss Link in Compensation - -The above step can have the following compensation function that dismisses the link between the records: - -export const compensationHighlights = [ - ["4", "resolve", "Resolve the remote link."], - ["8", "dismiss", "Create a link between two records."] -] - -```ts highlights={compensationHighlights} -export const linkProductToBrandStep = createStep( - // ... - async ({ productId, brandId }, { container }) => { - const remoteLink = container.resolve( - ContainerRegistrationKeys.REMOTE_LINK - ) - - remoteLink.dismiss({ - [Modules.PRODUCT]: { - product_id: productId, - }, - [BRAND_MODULE]: { - brand_id: brandId, - }, - }) - } -) -``` - -The `dismiss` method removes the link to dismiss between two records. Its parameter is the same as that of the `create` method. - ---- - -## Next Step: Extend Create Product API Route - -In the next step, you'll extend the Create Product API route to allow passing a brand ID, and link a product to a brand. \ No newline at end of file diff --git a/www/apps/book/app/learn/customization/extend-features/extend-create-product/page.mdx b/www/apps/book/app/learn/customization/extend-features/extend-create-product/page.mdx index 62148eb1221d6..8db53721fe149 100644 --- a/www/apps/book/app/learn/customization/extend-features/extend-create-product/page.mdx +++ b/www/apps/book/app/learn/customization/extend-features/extend-create-product/page.mdx @@ -1,26 +1,25 @@ import { Prerequisites } from "docs-ui" export const metadata = { - title: `${pageNumber} Extend Create Product API Route`, + title: `${pageNumber} Guide: Extend Create Product Flow`, } # {metadata.title} - +After linking the [custom Brand data model](../../custom-features/module/page.mdx) and Medusa's [Product Module](!resources!/commerce-modules/product) in the [previous chapter](../define-link/page.mdx), you'll extend the create product workflow and API route to allow associating a brand with a product. -This chapter covers how to extend the Create Product API route to link a product to a brand as a step of the ["Extend Models" chapter](../page.mdx). +Some API routes, including the [Create Product API route](!api!/admin#products_postproducts), accept an `additional_data` request body parameter. This parameter can hold custom data that's passed to the [hooks](../../../advanced-development/workflows/workflow-hooks/page.mdx) of the workflow executed in the API route, allowing you to consume those hooks and perform actions with the custom data. - - -## Additional Data in API Routes +So, in this chapter, to extend the create product flow and associate a brand with a product, you will: -Some API routes, including the [Create Product API route](!api!/admin#products_postproducts), accept an `additional_data` request body parameter. +- Consume the [productsCreated](!resources!/references/medusa-workflows/createProductsWorkflow#productsCreated) hook of the [createProductsWorkflow](!resources!/references/medusa-workflows/createProductsWorkflow), which is executed within the workflow after the product is created. You'll link the product with the brand passed in the `additional_data` parameter. +- Extend the Create Product API route to allow passing a brand ID in `additional_data`. -It's useful when you want to pass custom data, such as the brand ID, then perform an action based on this data, such as link the brand to the product. + ---- +To learn more about the `additional_data` property and the API routes that accept additional data, refer to [this chapter](../../../advanced-development/api-routes/additional-data/page.mdx). -## 1. Allow Passing the Brand ID in Additional Data + -Before passing custom properties in the `additional_data` parameter, you add the property to `additional_data`'s validation rules. - -Create the file `src/api/middlewares.ts`, which is a special file that defines middlewares or validation rules of custom properties passed in the `additional_data` parameter: - -```ts title="src/api/middlewares.ts" -import { defineMiddlewares } from "@medusajs/medusa" -import { z } from "zod" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/admin/products", - method: ["POST"], - additionalDataValidator: { - brand_id: z.string().optional(), - }, - }, - ], -}) -``` - -You use [Zod](https://zod.dev/) to add a validation rule to the `additional_data` parameter indicating that it can include a `brand_id` property of type string. +--- -### defineMiddleware Parameters +## 1. Consume the productCreated Hook -The `defineMiddlewares` function accepts an object having a `routes` property. Its value is an array of middleware route objects, each having the following properties: +A workflow hook is a point in a workflow where you can inject a step to perform a custom functionality. Consuming a workflow hook allows you to extend the features of a workflow and, consequently, the API route that uses it. -- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. It must be compatible with [path-to-regexp](https://github.com/pillarjs/path-to-regexp). -- `method`: An array of HTTP method to apply the middleware or additional data validation to. If not supplied, it's applied to all HTTP methods. -- `additionalDataValidator`: An object of key-value pairs defining the validation rules for custom properties using [Zod](https://zod.dev/). + ---- +Learn more about the workflow hooks in [this chapter](../../../advanced-development/workflows/workflow-hooks/page.mdx). -## 2. Link Brand to Product using Workflow Hook + -A workflow hook is a point in a workflow where you can inject a step to perform a custom functionality. This is useful to perform custom action in an API route's workflow. +The [createProductsWorkflow](!resources!/references/medusa-workflows/createProductsWorkflow) used in the [Create Product API route](!api!/admin#products_postproducts) has a `productsCreated` hook that runs after the product is created. You'll consume this hook to link the created product with the brand specified in the request paramters. -The [createProductsWorkflow](!resources!/references/medusa-workflows/createProductsWorkflow) used in the Create Product API route has a `productsCreated` hook that runs after the product is created. +To consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content: -So, to consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content: +![Directory structure after creating the hook's file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733384338/Medusa%20Book/brands-hook-dir-overview_ltwr5h.jpg) -export const hookHighlights = [ - ["7", "productsCreated", "Access the hook in the `hooks` property."], - ["9", "", "Only proceed if the brand ID is passed in the additional data."], +export const hook1Highlights = [ + ["8", "productsCreated", "Access the hook in the `hooks` property."], + ["9", "products", "The created products, passed from the workflow"], + ["9", "additional_data", "The custom data passed in the `additional_data` request body parameter."], ["18", "retrieveBrand", "Try to retrieve the brand to ensure it exists."], - ["27", "links", "Define an array to store the links in."], - ["31", "push", "Add a link to be created."], - ["41", "create", "Create the links."] ] -```ts title="src/workflows/hooks/created-product.ts" highlights={hookHighlights} +```ts title="src/workflows/hooks/created-product.ts" highlights={hook1Highlights} import { createProductsWorkflow } from "@medusajs/medusa/core-flows" import { StepResponse } from "@medusajs/framework/workflows-sdk" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" +import { Modules } from "@medusajs/framework/utils" +import { LinkDefinition } from "@medusajs/framework/types" import { BRAND_MODULE } from "../../modules/brand" import BrandModuleService from "../../modules/brand/service" @@ -98,71 +73,128 @@ createProductsWorkflow.hooks.productsCreated( return new StepResponse([], []) } - // check that brand exists const brandModuleService: BrandModuleService = container.resolve( BRAND_MODULE ) // if the brand doesn't exist, an error is thrown. await brandModuleService.retrieveBrand(additional_data.brand_id as string) - const remoteLink = container.resolve( - ContainerRegistrationKeys.REMOTE_LINK - ) - const logger = container.resolve( - ContainerRegistrationKeys.LOGGER - ) + // TODO link brand to product + }) +) +``` - const links = [] - - // link products to brands - for (const product of products) { - links.push({ - [Modules.PRODUCT]: { - product_id: product.id, - }, - [BRAND_MODULE]: { - brand_id: additional_data.brand_id, - }, - }) - } +Workflows have a special `hooks` property to access its hooks and consume them. Each hook, such as `productCreated`, accepts a step function as a parameter. The step function accepts the following parameters: + +1. An object having an `additional_data` property, which is the custom data passed in the request body under `additional_data`. The object will also have properties passed from the workflow to the hook, which in this case is the `products` property that holds an array of the created products. +2. An object of properties related to the step's context. It has a `container` property whose value is the [Medusa container](../../../basics/medusa-container/page.mdx) to resolve framework and commerce tools. - await remoteLink.create(links) +In the step, if a brand ID is passed in `additional_data`, you resolve the Brand Module's service and use its generated `retrieveBrand` method to retrieve the brand by its ID. The `retrieveBrand` method will throw an error if the brand doesn't exist. - logger.info("Linked brand to products") +### Link Brand to Product - return new StepResponse(links, links) +Next, you want to create a link between the created products and the brand. To do so, you use Remote Link, which is a class from the Modules SDK that provides methods to manage linked records. + + + +Learn more about the remote link in [this chapter](../../../advanced-development/module-links/remote-link/page.mdx). + + + +To use the remote link in the `productCreated` hook, replace the `TODO` with the following: + +export const hook2Highlights = [ + ["1", `"remoteLink"`, "Resolve the remote link from the container."] + ["4", "links", "Define an array to store the links in."], + ["7", "push", "Add a link to be created."], + ["17", "create", "Create the links."] +] + +```ts title="src/workflows/hooks/created-product.ts" highlights={hook2Highlights} +const remoteLink = container.resolve("remoteLink") +const logger = container.resolve("logger") + +const links: LinkDefinition[] = [] + +for (const product of products) { + links.push({ + [Modules.PRODUCT]: { + product_id: product.id, + }, + [BRAND_MODULE]: { + brand_id: additional_data.brand_id, + }, }) -) +} + +await remoteLink.create(links) + +logger.info("Linked brand to products") + +return new StepResponse(links, links) ``` -Workflows have a special `hooks` property to access its hooks and consume them. Each hook, such as `productCreated`, accept a step function as a parameter. +You resolve the remote link from the container. Then you loop over the created products to assemble an array of links to be created. After that, you pass the array of links to remote link's `create` method, which will link the product and brand records. -In the step, if a brand ID is passed in `additional_data` and the brand exists, you create a link between each product and the brand. +Each property in the link object is the name of a module, and its value is an object having a `{model_name}_id` property, where `{model_name}` is the snake-case name of the module's data model. Its value is the ID of the record to be linked. The link object's properties must be set in the same order as the link configurations passed to `defineLink`. + +![Diagram showcasing how the order of defining a link affects creating the link](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386156/Medusa%20Book/remote-link-brand-product-exp_fhjmg4.jpg) + +Finally, you return an instance of `StepResponse` returning the created links. ### Dismiss Links in Compensation -You can pass as a second parameter of the hook a compensation function that undoes what the step did. +You can pass as a second parameter of the hook a compensation function that undoes what the step did. It receives as a first parameter the returned `StepResponse`'s second parameter, and the step context object as a second parameter. -Add the following compensation function as a second parameter: +To undo creating the links in the hook, pass the following compensation function as a second parameter to `productsCreated`: ```ts title="src/workflows/hooks/created-product.ts" createProductsWorkflow.hooks.productsCreated( // ... (async (links, { container }) => { - if (!links.length) { + if (!links?.length) { return } - const remoteLink = container.resolve( - ContainerRegistrationKeys.REMOTE_LINK - ) + const remoteLink = container.resolve("remoteLink") await remoteLink.dismiss(links) }) ) ``` -In the compensation function, you dismiss the links created by the step using the `dismiss` method of the remote link. +In the compensation function, if the `links` parameter isn't empty, you resolve remote link from the container and use its `dismiss` method. This method removes a link between two records. It accepts the same parameter as the `create` method. + +--- + +## 2. Configure Additional Data Validation + +Now that you've consumed the `productCreated` hook, you want to configure the `/admin/products` API route that creates a new product to accept a brand ID in its `additional_data` parameter. + +You configure the properties accepted in `additional_data` in the `src/api/middlewares.ts` that exports middleware configurations. So, create `src/api/middlewares.ts` with the following content: + +![Directory structure after adding the middelwares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386868/Medusa%20Book/brands-middleware-dir-overview_uczos1.jpg) + +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/medusa" +import { z } from "zod" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/admin/products", + method: ["POST"], + additionalDataValidator: { + brand_id: z.string().optional(), + }, + }, + ], +}) +``` + +Objects in `routes` accept an `additionalDataValidator` property that configures the validation rules for custom properties passed in the `additional_data` request parameter. It accepts an object whose keys are custom property names, and their values are validation rules created using [Zod](https://zod.dev/). + +So, `POST` requests sent to `/admin/products` can now pass the ID of a brand in the `brand_id` property of `additional_data`. --- @@ -179,7 +211,7 @@ curl -X POST 'http://localhost:9000/auth/user/emailpass' \ }' ``` -Make sure to replace the email and password with your user's credentials. +Make sure to replace the email and password in the request body with your user's credentials. Then, send a `POST` request to `/admin/products` to create a product, and pass in the `additional_data` parameter a brand's ID: @@ -196,18 +228,12 @@ curl -X POST 'http://localhost:9000/admin/products' \ } ], "additional_data": { - "brand_id": "01J7AX9ES4X113HKY6C681KDZ2J" + "brand_id": "{brand_id}" } }' ``` - - -Make sure to replace the `{token}` in the Authorization header with the token received from the previous request. - - - -In the request body, you pass in the `additional_data` parameter a `brand_id`. +Make sure to replace `{token}` with the token you received from the previous request, and `{brand_id}` with the ID of a brand in your application. The request creates a product and returns it. @@ -215,14 +241,6 @@ In the Medusa application's logs, you'll find the message `Linked brand to produ --- -## Workflows and API Routes References - -Medusa exposes hooks in many of its workflows that you can consume to add custom logic. - -The [Store](!api!/store) and [Admin](!api!/admin) API references indicate what workflows are used in each API routes. By clicking on the workflow, you access the [workflow's reference](!resources!/medusa-workflows-reference) where you can see the hooks available in the workflow. - ---- - -## Next Steps: Query Linked Records +## Next Steps: Query Linked Brands and Products -In the next chapter, you'll learn how to query the brand linked to a product. +Now that you've extending the create-product flow to link a brand to it, you want to retrieve the brand details of a product. You'll learn how to do so in the next chapter. diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index ab8a217d3784a..25ecaa5498cf6 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -92,8 +92,7 @@ export const generatedEditDates = { "app/learn/customization/custom-features/api-route/page.mdx": "2024-11-28T13:12:10.521Z", "app/learn/customization/custom-features/module/page.mdx": "2024-11-28T09:25:29.098Z", "app/learn/customization/custom-features/workflow/page.mdx": "2024-11-28T10:47:28.084Z", - "app/learn/customization/extend-features/create-links/page.mdx": "2024-09-30T08:43:53.133Z", - "app/learn/customization/extend-features/extend-create-product/page.mdx": "2024-09-30T08:43:53.134Z", + "app/learn/customization/extend-features/extend-create-product/page.mdx": "2024-12-05T09:26:15.796Z", "app/learn/customization/custom-features/page.mdx": "2024-11-28T08:21:55.207Z", "app/learn/customization/customize-admin/page.mdx": "2024-09-12T12:25:29.853Z", "app/learn/customization/customize-admin/route/page.mdx": "2024-10-07T12:43:11.335Z", diff --git a/www/apps/book/next.config.mjs b/www/apps/book/next.config.mjs index a6a91336ba325..390cead21ebd4 100644 --- a/www/apps/book/next.config.mjs +++ b/www/apps/book/next.config.mjs @@ -177,6 +177,12 @@ const nextConfig = { destination: "/learn/customization/extend-features/:path*", permanent: true, }, + { + source: "/learn/customization/extend-features/create-links", + destination: + "/learn/customization/extend-features/extend-create-product", + permanent: true, + }, ] }, } diff --git a/www/apps/book/sidebar.mjs b/www/apps/book/sidebar.mjs index 18049baae31f4..f61486a6b5961 100644 --- a/www/apps/book/sidebar.mjs +++ b/www/apps/book/sidebar.mjs @@ -125,12 +125,7 @@ export const sidebar = numberSidebarItems( }, { type: "link", - title: "Create Links Between Records", - path: "/learn/customization/extend-features/create-links", - }, - { - type: "link", - title: "Extend Route", + title: "Extend Core Flow", path: "/learn/customization/extend-features/extend-create-product", }, {