docs: revise extend create product (#10444)

* docs: revise extend create product

* change sidebar title
This commit is contained in:
Shahed Nasser
2024-12-05 12:41:12 +02:00
committed by GitHub
parent 90d7f4ff39
commit 9690e44280
5 changed files with 166 additions and 263 deletions

View File

@@ -1,115 +0,0 @@
export const metadata = {
title: `${pageNumber} Create Links between Brand and Product Records`,
}
# {metadata.title}
<Note title="Example Chapter">
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).
</Note>
## What is the Remote Link?
The remote link is a class with utility methods to manage links between data models' records.
Its 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.
<Note title="Tip">
Use the `Modules` enum imported from `@medusajs/framework/utils` to for the commerce module's names.
</Note>
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.

View File

@@ -1,27 +1,26 @@
import { Prerequisites } from "docs-ui"
export const metadata = {
title: `${pageNumber} Extend Create Product API Route`,
title: `${pageNumber} Guide: Extend Create Product Flow`,
}
# {metadata.title}
<Note title="Example Chapter">
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.
So, in this chapter, to extend the create product flow and associate a brand with a product, you will:
- 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`.
<Note>
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).
</Note>
## Additional Data in API Routes
Some API routes, including the [Create Product API route](!api!/admin#products_postproducts), accept an `additional_data` request body parameter.
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.
---
## 1. Allow Passing the Brand ID in Additional Data
<Prerequisites
items={[
{
@@ -35,9 +34,146 @@ It's useful when you want to pass custom data, such as the brand ID, then perfor
]}
/>
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:
## 1. Consume the productCreated Hook
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.
<Note>
Learn more about the workflow hooks in [this chapter](../../../advanced-development/workflows/workflow-hooks/page.mdx).
</Note>
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.
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 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."],
]
```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 } from "@medusajs/framework/utils"
import { LinkDefinition } from "@medusajs/framework/types"
import { BRAND_MODULE } from "../../modules/brand"
import BrandModuleService from "../../modules/brand/service"
createProductsWorkflow.hooks.productsCreated(
(async ({ products, additional_data }, { container }) => {
if (!additional_data?.brand_id) {
return new StepResponse([], [])
}
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)
// TODO link brand to product
})
)
```
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.
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.
### Link Brand to Product
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.
<Note>
Learn more about the remote link in [this chapter](../../../advanced-development/module-links/remote-link/page.mdx).
</Note>
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)
```
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.
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. It receives as a first parameter the returned `StepResponse`'s second parameter, and the step context object 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) {
return
}
const remoteLink = container.resolve("remoteLink")
await remoteLink.dismiss(links)
})
)
```
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"
@@ -56,113 +192,9 @@ export default defineMiddlewares({
})
```
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.
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/).
### defineMiddleware Parameters
The `defineMiddlewares` function accepts an object having a `routes` property. Its value is an array of middleware route objects, each having the following properties:
- `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/).
---
## 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 has a `productsCreated` hook that runs after the product is created.
So, to consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content:
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."],
["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}
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
import { StepResponse } from "@medusajs/framework/workflows-sdk"
import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { BRAND_MODULE } from "../../modules/brand"
import BrandModuleService from "../../modules/brand/service"
createProductsWorkflow.hooks.productsCreated(
(async ({ products, additional_data }, { container }) => {
if (!additional_data?.brand_id) {
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
)
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,
},
})
}
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.
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.
### Dismiss Links in Compensation
You can pass as a second parameter of the hook a compensation function that undoes what the step did.
Add the following compensation function as a second parameter:
```ts title="src/workflows/hooks/created-product.ts"
createProductsWorkflow.hooks.productsCreated(
// ...
(async (links, { container }) => {
if (!links.length) {
return
}
const remoteLink = container.resolve(
ContainerRegistrationKeys.REMOTE_LINK
)
await remoteLink.dismiss(links)
})
)
```
In the compensation function, you dismiss the links created by the step using the `dismiss` method of the remote link.
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}"
}
}'
```
<Note title="Tip">
Make sure to replace the `{token}` in the Authorization header with the token received from the previous request.
</Note>
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
## Next Steps: Query Linked Brands and Products
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
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.

View File

@@ -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",

View File

@@ -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,
},
]
},
}

View File

@@ -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",
},
{