docs: customization chapter exploration (#9078)
Adds a new customizations chapter with realistic example while maintaining the linear learning journey. Preview: https://docs-v2-git-docs-customizations-chapter-medusajs.vercel.app/v2/customization
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
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.
|
||||
|
||||
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 = [
|
||||
["14", "resolve", "Resolve the remote link."],
|
||||
["18", "create", "Create a link between two records."]
|
||||
]
|
||||
|
||||
```ts highlights={stepHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import {
|
||||
Modules,
|
||||
ContainerRegistrationKeys,
|
||||
} from "@medusajs/utils"
|
||||
import { BRAND_MODULE } from "../../modules/brand"
|
||||
|
||||
export const linkProductToBrandStep = createStep(
|
||||
"link-product-to-brand",
|
||||
async ({ productId, brandId }, { 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/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.
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Define Link Between a Brand and a Product`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to define a link between the `Brand` and `Product`data models as a step of the ["Extend Models" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## 1. Define the Link Between Product and Brand
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module having a Brand data model",
|
||||
link: "/customization/custom-features/module"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines and exports the link using the `defineLink` function imported from `@medusajs/utils`.
|
||||
|
||||
So, create the file `src/links/product-brand.ts` with the following content:
|
||||
|
||||
export const highlights = [
|
||||
["7", "linkable", "Special `linkable` property that holds the linkable data models of `ProductModule`."],
|
||||
["10", "linkable", "Special `linkable` property that holds the linkable data models of `BrandModule`."],
|
||||
]
|
||||
|
||||
```ts title="src/links/product-brand.ts" highlights={highlights}
|
||||
import BrandModule from "../modules/brand"
|
||||
import ProductModule from "@medusajs/product"
|
||||
import { defineLink } from "@medusajs/utils"
|
||||
|
||||
export default defineLink(
|
||||
{
|
||||
linkable: ProductModule.linkable.product,
|
||||
isList: true,
|
||||
},
|
||||
BrandModule.linkable.brand
|
||||
)
|
||||
```
|
||||
|
||||
The `defineLink` function accepts two parameters, each specifying the link configurations of each data model.
|
||||
|
||||
Modules have a special `linkable` property that holds the data models' link configurations.
|
||||
|
||||
`defineLink` accepts for each parameter either:
|
||||
|
||||
- The data model's link configuration;
|
||||
- Or an object that has two properties:
|
||||
- `linkable`: the link configuration of the data model.
|
||||
- `isList`: Whether many records of the data model can be linked to the other model.
|
||||
|
||||
So, in the above code snippet, you define a link between the `Product` and `Brand` data models. Since `isList` is enabled on the product's side, a brand can be associated with multiple products.
|
||||
|
||||
---
|
||||
|
||||
## 2. Sync the Link to the Database
|
||||
|
||||
To reflect your link in the database, run the `db:sync-links` command:
|
||||
|
||||
```bash
|
||||
npx medusa db:sync-links
|
||||
```
|
||||
|
||||
This creates a table for the link in the database. The table stores the IDs of linked brand and product records.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
You can also use the `db:migrate` command, which both runs the migrations and syncs the links.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Next: Link Brand and Product Records
|
||||
|
||||
In the next chapter, you'll learn how to associate brand and product records by creating a link between them.
|
||||
@@ -0,0 +1,214 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Extend Create Product API Route`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
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).
|
||||
|
||||
</Note>
|
||||
|
||||
## Additional Data in API Routes
|
||||
|
||||
Some API routes, including the [Create Product API route](https://docs.medusajs.com/v2/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={[
|
||||
{
|
||||
text: "Brand Module",
|
||||
link: "/customization/custom-features/module"
|
||||
},
|
||||
{
|
||||
text: "Defined link between the Brand and Product data models.",
|
||||
link: "/customization/extend-models/define-link"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
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
|
||||
|
||||
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 = [
|
||||
["6", "productsCreated", "Access the hook in the `hooks` property."],
|
||||
["8", "", "Only proceed if the brand ID is passed in the additional data."],
|
||||
["17", "retrieveBrand", "Try to retrieve the brand to ensure it exists."],
|
||||
["21", "links", "Define an array to store the links in."],
|
||||
["25", "push", "Add a link to be created."],
|
||||
["35", "create", "Create the links."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/hooks/created-product.ts" highlights={hookHighlights}
|
||||
import { createProductsWorkflow } from "@medusajs/core-flows"
|
||||
import { Modules } from "@medusajs/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 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)
|
||||
|
||||
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.dimiss(links)
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
In the compensation function, you dismiss the links created by the step using the `dismiss` method of the remote link.
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test it out, first, retrieve the authentication token of your admin user by sending a `POST` request to `/auth/user/emailpass`:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/auth/user/emailpass' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"email": "admin@medusa-test.com",
|
||||
"password": "supersecret"
|
||||
}'
|
||||
```
|
||||
|
||||
Make sure to replace the email and password 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:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/admin/products' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer {token}' \
|
||||
--data '{
|
||||
"title": "Product 1",
|
||||
"additional_data": {
|
||||
"brand_id": "01J7AX9ES4X113HKY6C681KDZ2J"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
<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`.
|
||||
|
||||
The request creates a product and returns it.
|
||||
|
||||
In the Medusa application's logs, you'll find the message `Linked brand to products`, indicating that the workflow hook handler ran and linked the brand to the products.
|
||||
|
||||
---
|
||||
|
||||
## Worflows 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
|
||||
|
||||
In the next chapter, you'll learn how to query the brand linked to a product.
|
||||
@@ -0,0 +1,40 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} How to Extend Data Models`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about Medusa's alternative approach to extending data models.
|
||||
|
||||
## Extend Models Alternative: Module Links
|
||||
|
||||
Since modules are isolated from one another, it's not possible to directly extend a module's data models.
|
||||
|
||||
Instead, you define a link between the modules' data models.
|
||||
|
||||
### Why are Modules Isolated?
|
||||
|
||||
Some of the module isolation's benefits include:
|
||||
|
||||
- Integrate your module into any Medusa application without side-effects to your setup.
|
||||
- Replace existing modules with your custom implementation, if your use case is drastically different.
|
||||
- Use modules in other environments, such as Edge functions and Next.js apps.
|
||||
|
||||
### How does Medusa Manage Module Links?
|
||||
|
||||
When you define a link, the Medusa application creates a table in the database for it.
|
||||
|
||||
Then, when you create a link between two records, the Medusa application stores the IDs of the linked records in that table.
|
||||
|
||||
Medusa also provides the necessary tools to manage and query the linked records, which you'll learn about in the next chapters.
|
||||
|
||||
---
|
||||
|
||||
## Next Chapters: Link Brands to Products Example
|
||||
|
||||
The next chapters continue the brands example. It shows you how to:
|
||||
|
||||
- Link a brand, which you defined in a [previous example](../custom-features/module/page.mdx), to a product.
|
||||
- Manage linked records between the brands and products.
|
||||
- Extend Medusa's Create Product API route to link a product to a brand.
|
||||
- Query linked brands and products.
|
||||
@@ -0,0 +1,183 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Retrieve Brand linked to Product using Query`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to retrieve the brand linked to a product using Query as a step of the ["Extend Models" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## What is Query?
|
||||
|
||||
Query is a utility that retrieves data across modules and their links. It’s registered in the Medusa container under the `ContainerRegistrationKeys.QUERY` (`query`) registration name.
|
||||
|
||||
---
|
||||
|
||||
## Retrieve Brand of Product API Route
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module",
|
||||
link: "/customization/custom-features/module"
|
||||
},
|
||||
{
|
||||
text: "Defined link between the Brand and Product data models.",
|
||||
link: "/customization/extend-models/define-link"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
You'll create an API route that retrieves the brand of a product. You'll use this in a later chapter.
|
||||
|
||||
Create the file `src/api/admin/products/[id]/brand/route.ts` with the following content:
|
||||
|
||||
export const highlights = [
|
||||
["13", "resolve", "Resolve Query from the Medusa Container."],
|
||||
["17", "graph", "Run a query to retrieve a product by its ID and its brand."],
|
||||
["18", "entity", "The name of the model to query."],
|
||||
["19", "fields", "The fields and relations to retrieve."],
|
||||
["20", "filters", "The filters to apply on the retrieved data."]
|
||||
]
|
||||
|
||||
```ts title="src/api/admin/products/[id]/brand/route.ts" highlights={highlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const query = req.scope.resolve(
|
||||
ContainerRegistrationKeys.QUERY
|
||||
)
|
||||
|
||||
const { data: [product] } = await query.graph({
|
||||
entity: "product",
|
||||
fields: ["brand.*"],
|
||||
filters: {
|
||||
id: req.params.id,
|
||||
},
|
||||
})
|
||||
|
||||
res.json({ brand: product.brand })
|
||||
}
|
||||
```
|
||||
|
||||
In this example, you retrieve a product by its ID with its brand, and return the brand in the response.
|
||||
|
||||
### query.graph Parameters
|
||||
|
||||
The `graph` method of Query runs a query to retrieve data. It accepts an object having the following properties:
|
||||
|
||||
- `entity`: The data model's name as specified in the first parameter of `model.define`.
|
||||
- `fields`: An array of properties and relations to retrieve. You can pass:
|
||||
- A property's name, such as `id`.
|
||||
- A relation or linked model's name, such as `brand`. You suffix the name with `.*` to retrieve all its properties.
|
||||
- `filters`: An object of filters to apply on the retrieved data model's properties.
|
||||
|
||||
<Note>
|
||||
|
||||
Filters currently don't work on models of another module, such as `brand` in this example.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test the API route out, first, retrieve the authentication token of your admin user by sending a `POST` request to `/auth/user/emailpass`:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/auth/user/emailpass' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"email": "admin@medusa-test.com",
|
||||
"password": "supersecret"
|
||||
}'
|
||||
```
|
||||
|
||||
Make sure to replace the email and password with your user's credentials.
|
||||
|
||||
Then, send a `GET` request to `/admin/products/:id/brand`:
|
||||
|
||||
```bash
|
||||
curl 'http://localhost:9000/admin/product/prod_123/brand' \
|
||||
-H 'Authorization: Bearer {token}'
|
||||
```
|
||||
|
||||
This returns the product's brand if it has one. For example:
|
||||
|
||||
```json title="Example Response"
|
||||
{
|
||||
"brand": {
|
||||
"id": "123",
|
||||
"name": "Acme",
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Retrieve Products of a Brand
|
||||
|
||||
An example of retrieving the products of a brand:
|
||||
|
||||
export const brandProductsHighlights = [
|
||||
["7", `"products.*"`, "Use the plural name of `product` since a brand has multiple products."]
|
||||
]
|
||||
|
||||
```ts highlights={brandProductsHighlights}
|
||||
const query = req.scope.resolve(
|
||||
ContainerRegistrationKeys.QUERY
|
||||
)
|
||||
|
||||
const { data: [brand] } = await query.graph({
|
||||
entity: "brand",
|
||||
fields: ["products.*"],
|
||||
filters: {
|
||||
id: req.params.id,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
In this case, since a brand has multiple products, you specify the plural name of the `Product` data model (`products`) in `fields`.
|
||||
|
||||
The retrieved `brand` now has a `products` field, which is an array of products linked to it:
|
||||
|
||||
```json title="Example Response"
|
||||
{
|
||||
"brand": {
|
||||
"products": [
|
||||
// ...
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
By following the examples of the previous chapters, you:
|
||||
|
||||
- Defined a link between the Brand and Product modules's data models, as if you're extending the `Product` model to add a brand.
|
||||
- Created a link between brand and product records.
|
||||
- Queried the brand linked to a product, and vice versa.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
In the next chapters, you'll learn how to customize the Medusa Admin to show brands.
|
||||
Reference in New Issue
Block a user