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,114 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Create Brand API Route`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to define an API route that creates a brand as the last step of the ["Build Custom Features" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "createBrandWorkflow",
|
||||
link: "/customization/custom-features/workflow"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
Create the file `src/api/admin/brands/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/admin/brands/route.ts" collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
CreateBrandInput,
|
||||
createBrandWorkflow,
|
||||
} from "../../../workflows/create-brand"
|
||||
|
||||
export const POST = async (
|
||||
req: MedusaRequest<CreateBrandInput>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const { result } = await createBrandWorkflow(req.scope)
|
||||
.run({
|
||||
input: req.body,
|
||||
})
|
||||
|
||||
res.json({ brand: result })
|
||||
}
|
||||
```
|
||||
|
||||
This adds a `POST` API route at `/admin/brands`. In the API route's handler, you execute the `createBrandWorkflow`, passing it the request body as input.
|
||||
|
||||
You return in the response the created brand.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about API routes [in this guide](../../../basics/api-routes/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Test API Route
|
||||
|
||||
To test it out, first, retrieve an authenticated token of your admin user by sending a `POST` request to the `/auth/user/emailpass` API Route:
|
||||
|
||||
```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/brands`, passing the token received from the previous request in the `Authorization` header:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/admin/brands' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer {token}' \
|
||||
--data '{
|
||||
"name": "Acme"
|
||||
}'
|
||||
```
|
||||
|
||||
This returns the created brand in the response:
|
||||
|
||||
```json title="Example Response"
|
||||
{
|
||||
"brand": {
|
||||
"id": "01J7AX9ES4X113HKY6C681KDZJ",
|
||||
"name": "Acme",
|
||||
"created_at": "2024-09-09T08:09:34.244Z",
|
||||
"updated_at": "2024-09-09T08:09:34.244Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand by:
|
||||
|
||||
1. Creating a module that defines and manages the `Brand` data model.
|
||||
2. Creating a workflow that uses the module's main service to create a brand record, and implements the compensation logic to delete that brand in case an error occurs.
|
||||
3. Creating an API route that allows admin users to create a brand.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
In the next chapters, you'll learn how to extend data models and associate the brand with a product.
|
||||
128
www/apps/book/app/customization/custom-features/module/page.mdx
Normal file
128
www/apps/book/app/customization/custom-features/module/page.mdx
Normal file
@@ -0,0 +1,128 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Implement Brand Module`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to create a Brand Module as part of the ["Build Custom Features" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## 1. Create Module Directory
|
||||
|
||||
Start by creating the directory `src/modules/brand` that will hold the Brand Module's files.
|
||||
|
||||
---
|
||||
|
||||
## 2. Create Data Model
|
||||
|
||||
To create a data model that represents a new `brand` table in the database, create the file `src/modules/brand/models/brand.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/brand/models/brand.ts"
|
||||
import { model } from "@medusajs/utils"
|
||||
|
||||
export const Brand = model.define("brand", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text(),
|
||||
})
|
||||
```
|
||||
|
||||
This creates a `Brand` data model which has an `id` primary key property, and a `name` text property.
|
||||
|
||||
---
|
||||
|
||||
## 3. Create Module Service
|
||||
|
||||
Next, you'll create the module's main service that manages the `Brand` data model.
|
||||
|
||||
Create the file `src/modules/brand/service.ts` with the following content:
|
||||
|
||||
export const serviceHighlights = [
|
||||
["4", "MedusaService", "A service factory that generates data-management methods."]
|
||||
]
|
||||
|
||||
```ts title="src/modules/brand/service.ts" highlights={serviceHighlights}
|
||||
import { MedusaService } from "@medusajs/utils"
|
||||
import { Brand } from "./models/brand"
|
||||
|
||||
class BrandModuleService extends MedusaService({
|
||||
Brand,
|
||||
}) {
|
||||
|
||||
}
|
||||
|
||||
export default BrandModuleService
|
||||
```
|
||||
|
||||
The `BrandModuleService` extends a `MedusaService` function imported from `@medusajs/utils` which is a service factory.
|
||||
|
||||
The `MedusaService` function receives an object of the module's data models as a parameter, and generates methods to manage those data models, such as `createBrands` and `updateBrands`.
|
||||
|
||||
Those methods are now available at the `BrandModuleService` class and you'll use them in upcoming steps.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Find a reference of the generated methods in [this guide](!resources!/service-factory-reference).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## 4. Create Module's Definition
|
||||
|
||||
To export the module's definition, create the file `src/modules/brand/index.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/brand/index.ts"
|
||||
import { Module } from "@medusajs/utils"
|
||||
import BrandModuleService from "./service"
|
||||
|
||||
export const BRAND_MODULE = "brandModuleService"
|
||||
|
||||
export default Module(BRAND_MODULE, {
|
||||
service: BrandModuleService,
|
||||
})
|
||||
```
|
||||
|
||||
This exposes the module to your application and allows you to resolve the `BrandModuleService`, which is its main service.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about modules and services [in this guide](../../../basics/modules-and-services/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## 5. Register Module in Config
|
||||
|
||||
Finally, add the module to Medusa's configurations in `medusa-config.js`:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
module.exports = defineConfig({
|
||||
// ...
|
||||
modules: {
|
||||
brandModuleService: {
|
||||
resolve: "./modules/brand",
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Generate and Run Migrations
|
||||
|
||||
To reflect the data model in the database, generate migrations for the `brandModuleService` module and migrate the changes to the database:
|
||||
|
||||
```bash
|
||||
npx medusa db:generate brandModuleService
|
||||
npx medusa db:migrate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Step: Create Brand Workflow
|
||||
|
||||
In the next step, you'll create a workflow whose steps use the Brand Module's main service to create a brand.
|
||||
25
www/apps/book/app/customization/custom-features/page.mdx
Normal file
25
www/apps/book/app/customization/custom-features/page.mdx
Normal file
@@ -0,0 +1,25 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Build Custom Features`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about the concepts you need to build custom features in your Medusa application.
|
||||
|
||||
To add a custom feature to your application, you create:
|
||||
|
||||
1. A module with data models and a main service to manage them.
|
||||
2. A workflow to create, update, and delete records of data models. You implement functionalities in a workflow to benefit from features such as roll-back in case of errors, retry configurations, and more.
|
||||
3. An API route that exposes the workflow's functionality to clients, such as the storefront or admin dashboard.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Next Chapters: Brand Module Example
|
||||
|
||||
In the next chapters, you'll follow an example to:
|
||||
|
||||
1. Add a Brand Module that creates a `Brand` data model and provides data-management features.
|
||||
2. Add a workflow to create a brand.
|
||||
3. Expose an API route that allows admin users to create a brand using the workflow.
|
||||
@@ -0,0 +1,152 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Define Workflow to Create a Brand`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to define a workflow that creates a brand as part of the ["Build Custom Features" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## Workflows vs Services: Why use Workflows?
|
||||
|
||||
When manipulating data, use workflows instead of invoking a service's methods directly in your API route or other customizations.
|
||||
|
||||
Workflows eliminate data inconsistency in your application with its compensation mechanism that undoes changes if an error occurs. For example, if a workflow's step creates a brand, it also defines a compensation mechanism to remove the brand if an error occurs.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about workflows [in this guide](../../../basics/workflows/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
This is even more useful when you create workflows with many steps, or integrate third-party systems.
|
||||
|
||||
---
|
||||
|
||||
## Create createBrandWorkflow
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module",
|
||||
link: "/customization/custom-features/module"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
Create the file `src/workflows/create-brand/index.ts` with the following content:
|
||||
|
||||
```ts
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
|
||||
export type CreateBrandInput = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export const createBrandWorkflow = createWorkflow(
|
||||
"create-brand",
|
||||
(input: CreateBrandInput) => {
|
||||
// TODO
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
For now, this workflow only defines its input. You'll create its step and use it in the workflow.
|
||||
|
||||
---
|
||||
|
||||
## Create createBrandStep
|
||||
|
||||
Create the file `src/workflows/create-brand/steps/create-brand.ts` with the following content:
|
||||
|
||||
```ts title="src/workflows/create-brand/steps/create-brand.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { CreateBrandInput } from ".."
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
|
||||
export const createBrandStep = createStep(
|
||||
"create-brand-step",
|
||||
async (input: CreateBrandInput, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
const brand = await brandModuleService.createBrands(input)
|
||||
|
||||
return new StepResponse(brand, brand.id)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This defines a `createBrandStep`. In the step, you resolve the Brand Module's main service and use its generated `createBrands` method, which accepts one or more objects of brands to create.
|
||||
|
||||
The step returns the created brand in the first parameter of the `StepResponse`'s constructor.
|
||||
|
||||
### Add Compensation Function to Step
|
||||
|
||||
A compensation function rolls back changes made by the step if an error occurs in the workflow.
|
||||
|
||||
The second parameter of the `StepResponse`'s constructor is passed to the compensation function.
|
||||
|
||||
To add the compensation function, pass a third parameter to `createStep`:
|
||||
|
||||
```ts title="src/workflows/create-brand/steps/create-brand.ts"
|
||||
export const createBrandStep = createStep(
|
||||
// ...
|
||||
async (id: string, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
await brandModuleService.deleteBrands(id)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
You resolve the Brand Module's main service and use its generated `deleteBrands` method to delete the brand created by the step.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
The `deleteBrands` method accepts an ID or an array of IDs of brands to delete.
|
||||
|
||||
</Note>
|
||||
|
||||
So, when an error occurs during the workflow, the brand that was created by the step is deleted to maintain data consistency.
|
||||
|
||||
---
|
||||
|
||||
## Add Step to Workflow
|
||||
|
||||
Go back to the workflow at `src/workflows/create-brand/index.ts` and import the step you created:
|
||||
|
||||
```ts
|
||||
import { createBrandStep } from "./steps/create-brand"
|
||||
```
|
||||
|
||||
Then, replace the `TODO` with the following:
|
||||
|
||||
```ts
|
||||
const brand = createBrandStep(input)
|
||||
|
||||
return new WorkflowResponse(brand)
|
||||
```
|
||||
|
||||
You use the `createBrandStep` to create the brand and return it in the workflow's response.
|
||||
|
||||
---
|
||||
|
||||
## Next Step: Create Brand API Route
|
||||
|
||||
In the next step, you'll create an API route that allows admin users to create a brand using this workflow.
|
||||
10
www/apps/book/app/customization/customize-admin/page.mdx
Normal file
10
www/apps/book/app/customization/customize-admin/page.mdx
Normal file
@@ -0,0 +1,10 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Customize Admin to Add Brands`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you'll continue with the brands example to learn how to customize the Medusa Admin to:
|
||||
|
||||
- Show a product's brand on its details page using a widget.
|
||||
- Add a page showing the list of brands in your application using a UI route.
|
||||
186
www/apps/book/app/customization/customize-admin/route/page.mdx
Normal file
186
www/apps/book/app/customization/customize-admin/route/page.mdx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Create Brands List UI Route in Admin`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to create a UI route (or page) that shows your brands as a step of the ["Customize Admin" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## What is a UI Route?
|
||||
|
||||
A UI route is a React Component that adds a new page to your admin dashboard.
|
||||
|
||||
The UI Route can be shown in the sidebar or added as a nested page.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisite: Add Retrieve Brand API Route
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module",
|
||||
link: "/customization/custom-features/module"
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
Before adding the UI route, you need an API route that retrieves all brands.
|
||||
|
||||
Create the file `src/api/admin/brands/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/admin/brands/route.ts" collapsibleLines="1-7" expandMoreButton="Show Imports"
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const brandModuleService: BrandModuleService = req.scope.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
const limit = req.query.limit || 15
|
||||
const offset = req.query.offset || 0
|
||||
|
||||
const [brands, count] = await brandModuleService.listAndCountBrands({}, {
|
||||
skip: offset as number,
|
||||
take: limit as number,
|
||||
})
|
||||
|
||||
res.json({
|
||||
brands,
|
||||
count,
|
||||
limit,
|
||||
offset,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This adds a `GET` API route at `/admin/brands`.
|
||||
|
||||
In the API route, you resolve the Brand Module's main service and use its `listAndCountBrands` method to retrieve the list of brands with their total count.
|
||||
|
||||
This method accepts as a first parameter filters to apply on the retrieved data, and as a second parameter configurations for pagination.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about the `listAndCount` method and its parameters in [this reference](!resources!service-factory-reference/methods/listAndCount).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Add a UI Route to Show Brands
|
||||
|
||||
A UI route is created in a file named `page.tsx` under subdirectories of the `src/admin/routes` directory. The file’s default export must be the UI route’s React component.
|
||||
|
||||
To create a UI route that shows the list of brands, create the file `src/admin/routes/brands/page.tsx` with the following content:
|
||||
|
||||
export const uiRouteHighlights = [
|
||||
["7", "brands", "State variable to store the brands."],
|
||||
["12", "fetch", "Retrieve the brands from the custom API route."]
|
||||
]
|
||||
|
||||
```tsx title="src/admin/routes/brands/page.tsx" highlights={uiRouteHighlights}
|
||||
import { Table, Container, Heading } from "@medusajs/ui"
|
||||
import { useEffect, useState } from "react"
|
||||
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||
import { TagSolid } from "@medusajs/icons"
|
||||
|
||||
const BrandsPage = () => {
|
||||
const [brands, setBrands] = useState<
|
||||
Record<string, string>[]
|
||||
>([])
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/admin/brands`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ brands: brandsData }) => {
|
||||
setBrands(brandsData)
|
||||
})
|
||||
}, [])
|
||||
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Heading level="h2">Brands</Heading>
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>ID</Table.HeaderCell>
|
||||
<Table.HeaderCell>Name</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{brands.map((brand) => (
|
||||
<Table.Row key={brand.id}>
|
||||
<Table.Cell>{brand.id}</Table.Cell>
|
||||
<Table.Cell>{brand.name}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default BrandsPage
|
||||
|
||||
// TODO export configuration
|
||||
```
|
||||
|
||||
This adds a new page in the admin at `http://localhost:9000/app/brands`.
|
||||
|
||||
In the UI route's component, you retrieve the brands from the `/admin/brands` API route. You show the brands in a table.
|
||||
|
||||
### Add UI Route to the Sidebar
|
||||
|
||||
To add the UI route to the sidebar, replace the `TODO` at the end of the file with the following:
|
||||
|
||||
```ts title="src/admin/routes/brands/page.tsx"
|
||||
export const config = defineRouteConfig({
|
||||
label: "Brands",
|
||||
icon: TagSolid,
|
||||
})
|
||||
```
|
||||
|
||||
You export a `config` variable defined using the `defineRouteConfig` utility.
|
||||
|
||||
This indicates that a new item should be added to the sidebar with the title `Brands` and an icon from the [Medusa Icons package](!ui!/icons/overview).
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test it out, start the Medusa application and login into the Medusa Admin.
|
||||
|
||||
You'll find a new "Brands" sidebar item. If you click on it, a new page opens showing the list of brands in your store.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
By following the examples of the previous chapters, you:
|
||||
|
||||
- Created a widget that showed the brand of a product in the Medusa Admin.
|
||||
- Created a UI route that showed the list of brands in the Medusa Admin.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
In the next chapters, you'll learn how to integrate third-party systems into your Medusa application to sync brands.
|
||||
108
www/apps/book/app/customization/customize-admin/widget/page.mdx
Normal file
108
www/apps/book/app/customization/customize-admin/widget/page.mdx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Show Brand of Product in Admin`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to show the brand of a product in the Medusa Admin using a widget as a step of the ["Customize Admin" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## Widget to Show Brand in Product Details
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Retrieve Brand of Product API Route",
|
||||
link: "/customization/extend-models/query-linked-records"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
To create a widget that shows a product's brand in its details page, create the file `src/admin/widgets/product-brand.tsx` with the following content:
|
||||
|
||||
export const highlights = [
|
||||
["7", "data", "Receive the product's details as a prop"],
|
||||
["9", "brand", "A state variable to store the brand"],
|
||||
["19", "fetch", "Retrieve the brand of a product using the custom API route"],
|
||||
["39", "zone", "Show the widget at the top of the product details page."]
|
||||
]
|
||||
|
||||
```tsx title="src/admin/widgets/product-brand.tsx" highlights={highlights}
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { DetailWidgetProps, AdminProduct } from "@medusajs/types"
|
||||
import { useEffect, useState } from "react"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
|
||||
const ProductBrandWidget = ({
|
||||
data,
|
||||
}: DetailWidgetProps<AdminProduct>) => {
|
||||
const [brand, setBrand] = useState<
|
||||
Record<string, string> | undefined
|
||||
>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
return
|
||||
}
|
||||
|
||||
fetch(`/admin/products/${data.id}/brand`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ brand }) => {
|
||||
setBrand(brand)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [loading])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Heading level="h2">Brand</Heading>
|
||||
{loading && <span>Loading...</span>}
|
||||
{brand && <span>Name: {brand.name}</span>}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductBrandWidget
|
||||
```
|
||||
|
||||
This adds a widget at the top of the product's details page.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about widgets [in this guide](../../../basics/admin-customizations/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
Widgets created in a details page receive the targetted item in a `data` prop. So, the `ProductBrandWidget` receives the product's details in the `data` prop.
|
||||
|
||||
In the widget, you fetch the product's brand from the `/admin/products/:id/brand` API route and display it.
|
||||
|
||||
<Note>
|
||||
|
||||
Admin customizations can use the [Medusa UI package](!ui!) to align your customizations with the admin's design.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
Start your Medusa application and go to a product's details page in the Medusa Admin, you'll find a new block at the top of the page showing the product's brand.
|
||||
|
||||
---
|
||||
|
||||
## Next Chapter: Add List of Brands Page
|
||||
|
||||
In the next chapter, you'll add a new page or UI route that displays the list of brands in your application.
|
||||
@@ -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.
|
||||
40
www/apps/book/app/customization/extend-models/page.mdx
Normal file
40
www/apps/book/app/customization/extend-models/page.mdx
Normal file
@@ -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.
|
||||
@@ -0,0 +1,213 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Brand Example: Handle Event to Sync Third-Party System`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to emit an event when a brand is created, listen to that event in a subscriber, and create the brand in the third-party system as a step of the ["Integrate Systems" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## 1. Emit Custom Event for Brand Creation
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module with createBrandWorkflow",
|
||||
link: "/customization/custom-features/workflow"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
To handle brand-creation event, you'll emit a custom event when a brand is created.
|
||||
|
||||
In the `createBrandWorkflow` defined in `src/workflows/create-brand/index.ts`, use the `emitEventStep` helper step imported from `@medusajs/core-flows` after the `createBrandStep`:
|
||||
|
||||
export const eventHighlights = [
|
||||
["13", "emitEventStep", "Emit an event."],
|
||||
["14", "eventName", "The event's name."],
|
||||
["15", "data", "The data to pass in the payload."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/create-brand/index.ts" highlights={eventHighlights}
|
||||
// other imports...
|
||||
import {
|
||||
emitEventStep,
|
||||
} from "@medusajs/core-flows"
|
||||
|
||||
// ...
|
||||
|
||||
export const createBrandWorkflow = createWorkflow(
|
||||
"create-brand",
|
||||
(input: CreateBrandInput) => {
|
||||
// ...
|
||||
|
||||
emitEventStep({
|
||||
eventName: "brand.created",
|
||||
data: {
|
||||
id: brand.id,
|
||||
},
|
||||
})
|
||||
|
||||
return new WorkflowResponse(brand)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `emitEventStep` accepts as a parameter an object having two properties:
|
||||
|
||||
- `eventName`: The name of the event to emit.
|
||||
- `data`: The data payload to emit with the event. This is useful for subscribers to access the created brand.
|
||||
|
||||
---
|
||||
|
||||
## 2. Create Sync to Third-Party System Workflow
|
||||
|
||||
Next, you'll create the workflow that syncs the created brand to the third-party system.
|
||||
|
||||
Create the file `src/workflows/sync-brand-to-system/index.ts` with the following content:
|
||||
|
||||
```ts title="src/workflows/sync-brand-to-system/index.ts"
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
|
||||
export type SyncBrandToSystemInput = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const syncBrandToSystemWorkflow = createWorkflow(
|
||||
"sync-brand-to-system",
|
||||
(input: SyncBrandToSystemInput) => {
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This defines an empty workflow and its expected input.
|
||||
|
||||
### Create createBrandInSystemStep
|
||||
|
||||
Next, create the step that syncs the brand in the file `src/workflows/sync-brand-to-system/steps/create-brand-in-system.ts`:
|
||||
|
||||
export const stepHighlights = [
|
||||
["18", "createBrand", "Create a brand in the third-party system."],
|
||||
["27", "deleteBrand", "Delete the brand in the third-party system if an error occurs."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/sync-brand-to-system/steps/create-brand-in-system.ts" highlights={stepHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { SyncBrandToSystemInput } from ".."
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
|
||||
export const createBrandInSystemStep = createStep(
|
||||
"create-brand-in-system",
|
||||
async ({ id }: SyncBrandToSystemInput, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
const brand = await brandModuleService.retrieveBrand(id)
|
||||
|
||||
await brandModuleService.client.createBrand(brand)
|
||||
|
||||
return new StepResponse(null, brand.id)
|
||||
},
|
||||
async (id, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
await brandModuleService.client.deleteBrand(id)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This step resolves the Brand Module's main service and uses its `client` property to access its internal service that integrates the third-party system.
|
||||
|
||||
In the step, you use the `createBrand` method of the client to create the brand in the third-party system.
|
||||
|
||||
In the compensation function, you undo the step's action using the `deleteBrand` method of the client.
|
||||
|
||||
### Add Step to Workflow
|
||||
|
||||
Finally, add the step to the `syncBrandToSystemWorkflow` in `src/workflows/sync-brand-to-system/index.ts`:
|
||||
|
||||
```ts title="src/workflows/sync-brand-to-system/index.ts"
|
||||
// other imports...
|
||||
import { createBrandInSystemStep } from "./steps/create-brand-in-system"
|
||||
|
||||
// ...
|
||||
|
||||
export const syncBrandToSystemWorkflow = createWorkflow(
|
||||
"sync-brand-to-system",
|
||||
(input: SyncBrandToSystemInput) => {
|
||||
createBrandInSystemStep(input)
|
||||
|
||||
return new WorkflowResponse(undefined)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The workflow now calls the step and returns an `undefined` result.
|
||||
|
||||
---
|
||||
|
||||
## 3. Handle brand.created Event
|
||||
|
||||
To handle the `brand.created` event, create a subscriber at `src/subscribers/brand-created.ts` with the following content:
|
||||
|
||||
```ts title="src/subscribers/brand-created.ts"
|
||||
import type {
|
||||
SubscriberConfig,
|
||||
SubscriberArgs,
|
||||
} from "@medusajs/medusa"
|
||||
import { syncBrandToSystemWorkflow } from "../workflows/sync-brand-to-system"
|
||||
|
||||
export default async function brandCreatedHandler({
|
||||
event: { data },
|
||||
container,
|
||||
}: SubscriberArgs<Record<string, string>>) {
|
||||
await syncBrandToSystemWorkflow(container).run({
|
||||
input: data,
|
||||
})
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: "brand.created",
|
||||
}
|
||||
```
|
||||
|
||||
The subscriber handler accesses the event payload in the `event.data` property of its object parameter.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about subscribers [in this guide](../../../basics/events-and-subscribers/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
It then executes the `syncBrandToSystemWorkflow`, passing it the ID of the brand to create in the third-party system.
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test it out, start the Medusa application and create a brand using the API route created in a [previous chapter](../../custom-features/api-route/page.mdx#test-api-route).
|
||||
|
||||
If you check the logs, you'll find the `brand.created` event was emitted, and that the request to the third-party system was simulated.
|
||||
|
||||
---
|
||||
|
||||
## Next Chapter: Sync Brand from Third-Party System to Medusa
|
||||
|
||||
In the next chapter, you'll learn how to sync brands in the third-party system into Medusa using a workflow and a scheduled job.
|
||||
|
||||
28
www/apps/book/app/customization/integrate-systems/page.mdx
Normal file
28
www/apps/book/app/customization/integrate-systems/page.mdx
Normal file
@@ -0,0 +1,28 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Integrate Third-Party Systems`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to integrate a third-party system into Medusa.
|
||||
|
||||
## How to Integrate a Third-Party System?
|
||||
|
||||
To integrate a third-party system into Medusa, you:
|
||||
|
||||
1. Implement the methods to interact with the system in a service. It can either be the main module's service, or an internal service in the module that's used by the main one.
|
||||
2. Implement in workflows custom features around the integration, such as sending data to the third-party system.
|
||||
- Workflows roll-back mechanism ensures data consistency. This is essential as you integrate multiple systems into your application.
|
||||
3. Use the workflow in other resources to expose or utilize the custom functionality.
|
||||
|
||||
---
|
||||
|
||||
## Next Chapters: Syncing Brands Example
|
||||
|
||||
In the next chapters, you'll implement an example of syncing brands with a third-party system, such as a Content Management System (CMS).
|
||||
|
||||
That requires:
|
||||
|
||||
1. Implementing the service that integrates the third-party system.
|
||||
2. Creating a brand in the third-party system when a brand is created in Medusa.
|
||||
2. Retrieving the brands from the third-party system to sync them with Medusa's brands at a scheduled interval.
|
||||
@@ -0,0 +1,309 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Schedule Syncing Brands from Third-Party System`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to use workflows and scheduled jobs to sync brands from the third-party system as the last step of the ["Integrate Systems" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## 1. Implement Syncing Workflow
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module",
|
||||
link: "/customization/custom-features/module"
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
Start by defining the workflow that syncs the brand from the third-party system.
|
||||
|
||||
The workflow has the following steps:
|
||||
|
||||
1. Retrieve brands from the third-party system.
|
||||
2. Create new brands in Medusa.
|
||||
3. Update existing brands in Medusa.
|
||||
|
||||
### Retrieve Brands Step
|
||||
|
||||
To create the step that retrieves the brands from the third-party service, create the file `src/workflows/sync-brands-from-system/steps/retrieve-brands-from-system.ts` with the following content:
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/steps/retrieve-brands-from-system.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
|
||||
export const retrieveBrandsFromSystemStep = createStep(
|
||||
"retrieve-brands-from-system",
|
||||
async (_, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
const brands = await brandModuleService.client.retrieveBrands()
|
||||
|
||||
return new StepResponse(brands)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this step, you resolve the Brand Module's main service from the container, and use its client service to retrieve the brands from the third-party system.
|
||||
|
||||
The step returns the retrieved brands.
|
||||
|
||||
### Create Brands Step
|
||||
|
||||
Next, create the step that creates new brands in Medusa in the file `src/workflows/sync-brands-from-system/steps/create-brands.ts`:
|
||||
|
||||
export const createBrandsHighlights = [
|
||||
["19", "createBrands", "Create the brands in Medusa"],
|
||||
["28", "deleteBrands", "Delete the brands from Medusa"]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/steps/create-brands.ts" highlights={createBrandsHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
|
||||
type CreateBrandsInput = {
|
||||
brands: Record<string, string>[]
|
||||
}
|
||||
|
||||
export const createBrandsStep = createStep(
|
||||
"create-brand-step",
|
||||
async (input: CreateBrandsInput, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
const brands = await brandModuleService.createBrands(input.brands)
|
||||
|
||||
return new StepResponse(brands, brands.map((brand) => brand.id))
|
||||
},
|
||||
async (ids: string[], { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
await brandModuleService.deleteBrands(ids)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This step receives the brands to create as input.
|
||||
|
||||
In the step, you resolve the Brand Module's main service and uses its `createBrands` method to create the brands.
|
||||
|
||||
You return the created brands and pass their IDs to the compensation function, which deletes the brands if an error occurs.
|
||||
|
||||
### Update Brands Step
|
||||
|
||||
To create the step that updates existing brands in Medusa, create the file `src/workflows/sync-brands-from-system/steps/update-brands.ts` with the following content:
|
||||
|
||||
export const updateBrandsHighlights = [
|
||||
["19", "prevUpdatedBrands", "Retrieve the data of the brands before the update."],
|
||||
["23", "updateBrands", "Update the brands in Medusa."],
|
||||
["32", "updateBrands", "Revert the update by reverting the brands' to before the update."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/steps/update-brands.ts" highlights={updateBrandsHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
|
||||
type UpdateBrandsInput = {
|
||||
brands: Record<string, string>[]
|
||||
}
|
||||
|
||||
export const updateBrandsStep = createStep(
|
||||
"update-brand-step",
|
||||
async ({ brands }: UpdateBrandsInput, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
const prevUpdatedBrands = await brandModuleService.listBrands({
|
||||
id: brands.map((brand) => brand.id),
|
||||
})
|
||||
|
||||
const updatedBrands = await brandModuleService.updateBrands(brands)
|
||||
|
||||
return new StepResponse(updatedBrands, prevUpdatedBrands)
|
||||
},
|
||||
async (prevUpdatedBrands, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
await brandModuleService.updateBrands(prevUpdatedBrands)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This step receives the brands to update as input.
|
||||
|
||||
In the step, you retrieve the brands first to pass them later to the compensation function, then update and return the brands.
|
||||
|
||||
In the compensation function, you update the brands are again but to their data before the update made by the step.
|
||||
|
||||
### Create Workflow
|
||||
|
||||
Finally, create the workflow in the file `src/workflows/sync-brands-from-system/index.ts` with the following content:
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/index.ts"
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
transform,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { retrieveBrandsFromSystemStep } from "./steps/retrieve-brands-from-system"
|
||||
import { createBrandsStep } from "./steps/create-brands"
|
||||
import { updateBrandsStep } from "./steps/update-brands"
|
||||
|
||||
export const syncBrandsFromSystemWorkflow = createWorkflow(
|
||||
"sync-brands-from-system",
|
||||
() => {
|
||||
const brands = retrieveBrandsFromSystemStep()
|
||||
|
||||
// TODO create and update brands
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
For now, you only add the `retrieveBrandsFromSystemStep` to the workflow that retrieves the brands from the third-party system.
|
||||
|
||||
### Identify Brands to Create or Update in Workflow
|
||||
|
||||
Next, you need to identify which brands must be created or updated.
|
||||
|
||||
Since workflows are constructed internally and are only evaluated during execution, you can't access any data's value to perform data manipulation or checks.
|
||||
|
||||
Instead, use the `transform` utility function imported from `@medusajs/workflows-sdk`, which gives you access to the real-time values of the data to perfrom actions on them.
|
||||
|
||||
So, replace the `TODO` with the following:
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/index.ts"
|
||||
const { toCreate, toUpdate } = transform(
|
||||
{
|
||||
brands,
|
||||
},
|
||||
(data) => {
|
||||
const toCreate: Record<string, string>[] = []
|
||||
const toUpdate: Record<string, string>[] = []
|
||||
|
||||
data.brands.forEach((brand) => {
|
||||
if (brand.external_id) {
|
||||
toUpdate.push({
|
||||
...brand,
|
||||
id: brand.external_id,
|
||||
})
|
||||
} else {
|
||||
toCreate.push(brand)
|
||||
}
|
||||
})
|
||||
|
||||
return { toCreate, toUpdate }
|
||||
}
|
||||
)
|
||||
|
||||
// TODO create and update the brands
|
||||
```
|
||||
|
||||
`transform` accepts two parameters:
|
||||
|
||||
1. The data to be passed to the function in the second parameter.
|
||||
2. A function to execute only when the workflow is executed. Its return value can be consumed by the rest of the workflow.
|
||||
|
||||
In the function, you sort the brands as to be created or to be updated based on whether they have an `external_id` property.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
This approach assumes that the third-party system stores the ID of the brand in Medusa in `external_id`.
|
||||
|
||||
</Note>
|
||||
|
||||
### Create and Update the Brands
|
||||
|
||||
Finally, replace the new `TODO` with the following:
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/index.ts"
|
||||
const created = createBrandsStep({ brands: toCreate })
|
||||
const updated = updateBrandsStep({ brands: toUpdate })
|
||||
|
||||
return new WorkflowResponse({
|
||||
created,
|
||||
updated,
|
||||
})
|
||||
```
|
||||
|
||||
You pass the brands to be created to the `createBrandsStep`, and the brands to be updated to the `updateBrandsStep`.
|
||||
|
||||
Then, you return the created and updated brands.
|
||||
|
||||
---
|
||||
|
||||
## 2. Schedule Syncing Task
|
||||
|
||||
To schedule a task that syncs brands from the third-party system, create a scheduled job at `src/jobs/sync-brands-from-system.ts`:
|
||||
|
||||
```ts title="src/jobs/sync-brands-from-system.ts"
|
||||
import { MedusaContainer } from "@medusajs/types"
|
||||
import { syncBrandsFromSystemWorkflow } from "../workflows/sync-brands-from-system"
|
||||
|
||||
export default async function (container: MedusaContainer) {
|
||||
const logger = container.resolve("logger")
|
||||
|
||||
const { result } = await syncBrandsFromSystemWorkflow(container).run()
|
||||
|
||||
logger.info(
|
||||
`Synced brands from third-party system: ${
|
||||
result.created.length
|
||||
} brands created and ${result.updated.length} brands updated.`)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
name: "sync-brands-from-system",
|
||||
schedule: "* * * * *",
|
||||
}
|
||||
```
|
||||
|
||||
This defines a scheduled job that runs every minute (for testing purposes).
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about scheduled jobs [in this guide](../../../basics/scheduled-jobs/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
The scheduled job executes the `syncBrandsFromSystemWorkflow` and prints how many brands were created and updated.
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test it out, start the Medusa application. In a minute, the scheduled job will run and you'll see a logged message indicating how many brands were created or updated.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
In the previous chapters, you:
|
||||
|
||||
- Created a service that acts as a client integrating a third-party system.
|
||||
- Implemented two-way sync of brands between the third-party system and Medusa using a subscriber and a scheduled job.
|
||||
@@ -0,0 +1,201 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Integrate Third-Party Brand System in a Service`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to integrate a dummy third-party system in a service as a step of the ["Integrate Systems" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## 1. Create Service
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module",
|
||||
link: "/customization/custom-features/module"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
Start by creating the file `src/modules/brand/services/client.ts` with the following content:
|
||||
|
||||
export const serviceHighlights = [
|
||||
["4", "BrandClientOptions", "Define the options that the Brand Module receives necessary for the integration."],
|
||||
["8", "InjectedDependencies", "Define the dependencies injected into the service."],
|
||||
["20", "moduleDef", "Retrieve the module's configuration."]
|
||||
]
|
||||
|
||||
```ts title="src/modules/brand/services/client.ts" highlights={serviceHighlights}
|
||||
import { Logger, ConfigModule } from "@medusajs/types"
|
||||
import { BRAND_MODULE } from ".."
|
||||
|
||||
export type BrandClientOptions = {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
type InjectedDependencies = {
|
||||
logger: Logger
|
||||
configModule: ConfigModule
|
||||
}
|
||||
|
||||
export class BrandClient {
|
||||
private options_: BrandClientOptions
|
||||
private logger_: Logger
|
||||
|
||||
constructor({ logger, configModule }: InjectedDependencies) {
|
||||
this.logger_ = logger
|
||||
|
||||
const moduleDef = configModule.modules[BRAND_MODULE]
|
||||
if (typeof moduleDef !== "boolean") {
|
||||
this.options_ = moduleDef.options
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This creates a `BrandClient` service. Using dependency injection, you resolve the `logger` and `configModule` from the Module's container.
|
||||
|
||||
`logger` is useful to log messages, and `configModule` has configurations exported in `medusa-config.js`.
|
||||
|
||||
You also define an `options_` property in your service to store the module's options.
|
||||
|
||||
The `configModule`'s `modules` property is an object whose keys are registered module names and values are the module's configuration.
|
||||
|
||||
If the module's configuration isn't a boolean, it has an `options` property that holds the module's options. You use it to set the `options_` property's value.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
If the service integrating the third-party system was a main service, it receives the module's options as a second parameter.
|
||||
|
||||
</Note>
|
||||
|
||||
### Integration Methods
|
||||
|
||||
Next, add the following methods to simulate sending requests to the third-party system:
|
||||
|
||||
export const methodsHighlights = [
|
||||
["6", "sendRequest", "Since the third-party system isn't real, this method only logs a message."],
|
||||
["15", "createBrand", "A method that creates a brand in the third-party system."],
|
||||
["19", "deleteBrand", "A method that deletes a brand in the third-party system."],
|
||||
["23", "retrieveBrands", "A method that retrieves a brand from a third-party system."]
|
||||
]
|
||||
|
||||
```ts title="src/modules/brand/services/client.ts" highlights={methodsHighlights}
|
||||
export class BrandClient {
|
||||
// ...
|
||||
|
||||
// a dummy method to simulate sending a request,
|
||||
// in a realistic scenario, you'd use an SDK, fetch, or axios clients
|
||||
private async sendRequest(url: string, method: string, data?: any) {
|
||||
this.logger_.info(`Sending a ${
|
||||
method
|
||||
} request to ${url}. data: ${JSON.stringify(data, null, 2)}`)
|
||||
this.logger_.info(`Client Options: ${
|
||||
JSON.stringify(this.options_, null, 2)
|
||||
}`)
|
||||
}
|
||||
|
||||
async createBrand(brand: Record<string, string>) {
|
||||
await this.sendRequest("/brands", "POST", brand)
|
||||
}
|
||||
|
||||
async deleteBrand(id: string) {
|
||||
await this.sendRequest(`/brands/${id}`, "DELETE")
|
||||
}
|
||||
|
||||
async retrieveBrands() {
|
||||
await this.sendRequest("/brands", "GET")
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `sendRequest` method is a dummy method to simulate sending a request to a third-party system.
|
||||
|
||||
You also add three methods that use the `sendRequest` method:
|
||||
|
||||
- `createBrand` that creates a brand in the third-party system.
|
||||
- `deleteBrand` that deletes the brand in the third-party system.
|
||||
- `retrieveBrands` to retrieve a brand from the third-party system.
|
||||
|
||||
---
|
||||
|
||||
## 2. Export Service
|
||||
|
||||
If the service integrating the third-party system is the module's main service, you only need to export it in the module definition.
|
||||
|
||||
However, since this service is an internal service in the Brand Module, you must export it in a `src/modules/brand/services/index.ts` file:
|
||||
|
||||
```ts title="src/modules/brand/services/index.ts"
|
||||
export * from "./client"
|
||||
```
|
||||
|
||||
This registers the service in the module's container, allowing you to access it in the module's main service.
|
||||
|
||||
---
|
||||
|
||||
## 3. Add Internal Service in Main Service
|
||||
|
||||
In the main service at `src/modules/brand/service.ts`, add the following imports and types at the top of the file:
|
||||
|
||||
```ts title="src/modules/brand/service.ts"
|
||||
// other imports...
|
||||
import { BrandClient, BrandClientOptions } from "./services"
|
||||
|
||||
type InjectedDependencies = {
|
||||
brandClient: BrandClient
|
||||
}
|
||||
```
|
||||
|
||||
Then, add the following in the `BrandModuleService` class:
|
||||
|
||||
```ts title="src/modules/brand/service.ts"
|
||||
class BrandModuleService extends MedusaService({
|
||||
Brand,
|
||||
}) {
|
||||
public client: BrandClient
|
||||
|
||||
constructor({ brandClient }: InjectedDependencies) {
|
||||
super(...arguments)
|
||||
|
||||
this.client = brandClient
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the main module service, you first resolve through dependency injection the `brandClient` from the container and set it in a public property `client`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Pass Options to the Module
|
||||
|
||||
To pass options in the module, change its configurations in `medusa-config.js`:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
module.exports = defineConfig({
|
||||
// ...
|
||||
modules: {
|
||||
brandModuleService: {
|
||||
resolve: "./modules/brand",
|
||||
options: {
|
||||
apiKey: process.env.BRAND_API_KEY || "temp",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
A module's configuration accepts an `options` property, which can hold any options to pass to the module.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps: Sync Brand From Medusa to Third-Party System
|
||||
|
||||
In the next chapter, you'll learn how to sync brands created in Medusa to the third-party system using a workflow and a scheduled job.
|
||||
38
www/apps/book/app/customization/next-steps/page.mdx
Normal file
38
www/apps/book/app/customization/next-steps/page.mdx
Normal file
@@ -0,0 +1,38 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Customizations Next Steps`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
The previous examples under the Customization chapter explained more about customizing Medusa for a realistic use case.
|
||||
|
||||
Your learning journey doesn't end here, and this only presents some of Medusa's powerful feature.
|
||||
|
||||
This chapter guides you into how to continue your learning journey, and what resources will be helpful for you during your development.
|
||||
|
||||
## Follow the Rest of this Documentation
|
||||
|
||||
The next chapters of the documentation provide more in-depth uses of the different concepts you learned about.
|
||||
|
||||
While you can start playing around with Medusa and customize it, it's highly recommended to continue the rest of this documentation to learn about what more you can do with each concept.
|
||||
|
||||
---
|
||||
|
||||
## Helpful Resources Guides
|
||||
|
||||
The [Learning Resources](!resources!) documentation provides more helpful guides and references for your development journey.
|
||||
|
||||
Some of these guides and references are:
|
||||
|
||||
1. [Service Factory Reference](!resources!/service-factory-reference): Learn about the methods generated by the service factory with examples.
|
||||
2. [Workflows Reference](!resources!/medusa-workflows-reference): Browse the list of workflows and their hooks.
|
||||
3. [Commerce Modules](!resources!/commerce-modules): Browse the list of commerce modules in Medusa and their references to learn how to use them.
|
||||
4. [Admin Injection Zones](!resources!/admin-widget-injection-zones): Browse the injection zones in the Medusa Admin to learn where you can inject widgets.
|
||||
|
||||
---
|
||||
|
||||
## More Examples in Recipes
|
||||
|
||||
In the Learning Resources documentation, you'll also find step-by-step guides of different use cases, such as building a marketplace, digital products, and more.
|
||||
|
||||
Refer to the [Recipes](!resources!/recipes) documentation to learn more.
|
||||
14
www/apps/book/app/customization/page.mdx
Normal file
14
www/apps/book/app/customization/page.mdx
Normal file
@@ -0,0 +1,14 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Custom Development`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you’ll customize Medusa using the concepts you learned.
|
||||
|
||||
You'll follow an example of building a Brands Module that leads you through:
|
||||
|
||||
1. The main concepts you need to build custom features.
|
||||
2. How to extend data models that are in the Commerce Modules.
|
||||
3. How to use your custom features in pages and widgets in the admin dashboard.
|
||||
4. How to integrate third-party systems into your Medusa application.
|
||||
@@ -81,5 +81,23 @@ export const generatedEditDates = {
|
||||
"app/advanced-development/modules/query/page.mdx": "2024-09-11T10:46:49.512Z",
|
||||
"app/debugging-and-testing/testing-tools/modules-tests/module-example/page.mdx": "2024-09-10T11:39:51.171Z",
|
||||
"app/debugging-and-testing/testing-tools/modules-tests/page.mdx": "2024-09-10T11:39:51.171Z",
|
||||
"app/customization/custom-features/api-route/page.mdx": "2024-09-12T12:42:34.201Z",
|
||||
"app/customization/custom-features/module/page.mdx": "2024-09-12T12:39:37.928Z",
|
||||
"app/customization/custom-features/workflow/page.mdx": "2024-09-12T12:40:39.582Z",
|
||||
"app/customization/extend-models/create-links/page.mdx": "2024-09-12T12:42:55.602Z",
|
||||
"app/customization/extend-models/extend-create-product/page.mdx": "2024-09-12T12:43:57.702Z",
|
||||
"app/customization/custom-features/page.mdx": "2024-09-12T11:18:13.271Z",
|
||||
"app/customization/customize-admin/page.mdx": "2024-09-12T12:25:29.853Z",
|
||||
"app/customization/customize-admin/route/page.mdx": "2024-09-12T12:45:39.258Z",
|
||||
"app/customization/customize-admin/widget/page.mdx": "2024-09-12T12:26:36.013Z",
|
||||
"app/customization/extend-models/define-link/page.mdx": "2024-09-12T12:38:53.230Z",
|
||||
"app/customization/extend-models/page.mdx": "2024-09-12T12:38:57.394Z",
|
||||
"app/customization/extend-models/query-linked-records/page.mdx": "2024-09-12T12:44:41.089Z",
|
||||
"app/customization/integrate-systems/handle-event/page.mdx": "2024-09-12T12:39:07.978Z",
|
||||
"app/customization/integrate-systems/page.mdx": "2024-09-12T12:33:29.827Z",
|
||||
"app/customization/integrate-systems/schedule-task/page.mdx": "2024-09-12T12:46:17.591Z",
|
||||
"app/customization/integrate-systems/service/page.mdx": "2024-09-12T12:39:12.831Z",
|
||||
"app/customization/next-steps/page.mdx": "2024-09-12T10:50:04.873Z",
|
||||
"app/customization/page.mdx": "2024-09-12T11:16:18.504Z",
|
||||
"app/debugging-and-testing/instrumentation/page.mdx": "2024-09-17T08:53:15.910Z"
|
||||
}
|
||||
@@ -83,6 +83,107 @@ export const sidebar = numberSidebarItems(
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/customization",
|
||||
title: "Custom Development",
|
||||
chapterTitle: "Customization",
|
||||
children: [
|
||||
{
|
||||
type: "link",
|
||||
title: "Build Custom Features",
|
||||
path: "/customization/custom-features",
|
||||
children: [
|
||||
{
|
||||
type: "link",
|
||||
title: "Brand Module",
|
||||
path: "/customization/custom-features/module",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
title: "Create Brand Workflow",
|
||||
path: "/customization/custom-features/workflow",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
title: "Create Brand API Route",
|
||||
path: "/customization/custom-features/api-route",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
title: "Extend Models",
|
||||
path: "/customization/extend-models",
|
||||
children: [
|
||||
{
|
||||
type: "link",
|
||||
title: "Define Link",
|
||||
path: "/customization/extend-models/define-link",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
title: "Create Links Between Records",
|
||||
path: "/customization/extend-models/create-links",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
title: "Extend Route",
|
||||
path: "/customization/extend-models/extend-create-product",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
title: "Query Linked Records",
|
||||
path: "/customization/extend-models/query-linked-records",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
title: "Customize Admin",
|
||||
path: "/customization/customize-admin",
|
||||
children: [
|
||||
{
|
||||
type: "link",
|
||||
title: "Add Widgets",
|
||||
path: "/customization/customize-admin/widget",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
title: "Add UI Route",
|
||||
path: "/customization/customize-admin/route",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
title: "Integrate Systems",
|
||||
path: "/customization/integrate-systems",
|
||||
children: [
|
||||
{
|
||||
type: "link",
|
||||
title: "Integrate with Service",
|
||||
path: "/customization/integrate-systems/service",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
title: "Handle Event",
|
||||
path: "/customization/integrate-systems/handle-event",
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
title: "Schedule Task",
|
||||
path: "/customization/integrate-systems/schedule-task",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
title: "Next Steps",
|
||||
path: "/customization/next-steps",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
path: "/advanced-development",
|
||||
|
||||
@@ -75,7 +75,7 @@ export const NoteLayout = ({ type, title, children }: NoteLayoutProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"py-[10px] px-docs_0.75 my-docs_0.5",
|
||||
"py-[10px] px-docs_0.75 my-docs_1",
|
||||
"flex gap-docs_0.75 rounded-docs_DEFAULT items-stretch",
|
||||
"bg-medusa-bg-component border border-medusa-border-base"
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user