docs: revise last chapters of customizations (#10480)

This commit is contained in:
Shahed Nasser
2024-12-06 17:56:27 +02:00
committed by GitHub
parent 21b0e0c26b
commit a76b533604
8 changed files with 450 additions and 339 deletions
@@ -1,31 +1,43 @@
import { Prerequisites } from "docs-ui"
export const metadata = {
title: `${pageNumber} Brand Example: Handle Event to Sync Third-Party System`,
title: `${pageNumber} Guide: Sync Brands from Medusa to CMS`,
}
# {metadata.title}
<Note title="Example Chapter">
In the [previous chapter](../service/page.mdx), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows.
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).
In another previous chapter, you [added a workflow](../../custom-features/workflow/page.mdx) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well.
Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](../../../basics/events-and-subscribers/page.mdx). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system.
<Note>
Learn more about Medusa's event system and subscribers in [this chapter](../../../basics/events-and-subscribers/page.mdx).
</Note>
## 1. Emit Custom Event for Brand Creation
In this chapter, you'll modify the `createBrandWorkflow` you created before to emit a custom event that indicates a brand was created. Then, you'll listen to that event in a subscriber to sync the brand to the third-party CMS. You'll implement the sync logic within a workflow that you execute in the subscriber.
<Prerequisites
items={[
{
text: "Brand Module with createBrandWorkflow",
text: "createBrandWorkflow",
link: "/learn/customization/custom-features/workflow"
},
{
text: "CMS Module",
link: "/learn/customization/integrate-systems/service"
}
]}
/>
To handle brand-creation event, you'll emit a custom event when a brand is created.
## 1. Emit Event in createBrandWorkflow
In the `createBrandWorkflow` defined in `src/workflows/create-brand/index.ts`, use the `emitEventStep` helper step imported from `@medusajs/medusa/core-flows` after the `createBrandStep`:
Since syncing the brand to the third-party system isn't integral to creating a brand, you'll emit a custom event indicating that a brand was created.
Medusa provides an `emitEventStep` that allows you to emit an event in your workflows. So, in the `createBrandWorkflow` defined in `src/workflows/create-brand.ts`, use the `emitEventStep` helper step after the `createBrandStep`:
export const eventHighlights = [
["13", "emitEventStep", "Emit an event."],
@@ -58,126 +70,183 @@ export const createBrandWorkflow = createWorkflow(
)
```
The `emitEventStep` accepts as a parameter an object having two properties:
The `emitEventStep` accepts an object parameter 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.
- `eventName`: The name of the event to emit. You'll use this name later to listen to the event in a subscriber.
- `data`: The data payload to emit with the event. This data is passed to subscribers that listen to the event. You add the brand's ID to the data payload, informing the subscribers which brand was created.
You'll learn how to handle this event in a later step.
---
## 2. Create Sync to Third-Party System Workflow
Next, you'll create the workflow that syncs the created brand to the third-party system.
The subscriber that will listen to the `brand.created` event will sync the created brand to the third-party CMS. So, you'll implement the syncing logic in a workflow, then execute the workflow in the subscriber.
Create the file `src/workflows/sync-brand-to-system/index.ts` with the following content:
Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution.
```ts title="src/workflows/sync-brand-to-system/index.ts"
import {
createWorkflow,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
<Note>
export type SyncBrandToSystemInput = {
id: string
}
Learn more about workflows in [this chapter](../../../basics/workflows/page.mdx).
export const syncBrandToSystemWorkflow = createWorkflow(
"sync-brand-to-system",
(input: SyncBrandToSystemInput) => {
// ...
}
)
```
</Note>
This defines an empty workflow and its expected input.
You'll create a `syncBrandToSystemWorkflow` that has two steps:
### Create createBrandInSystemStep
- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](../../../advanced-development/module-links/query/page.mdx). You'll use this to retrieve the brand's details using its ID.
- `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS.
Next, create the step that syncs the brand in the file `src/workflows/sync-brand-to-system/steps/create-brand-in-system.ts`:
### syncBrandToCmsStep
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."]
To implement the step that syncs the brand to the CMS, create the file `src/workflows/sync-brands-to-cms.ts` with the following content:
![Directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493547/Medusa%20Book/cms-dir-overview-4_u5t0ug.jpg)
export const syncStepHighlights = [
["8", "InferTypeOf", "Get the `Brand` data model as a type."],
["14", "cmsModuleService", "Resolve the CMS Module's service from the container."],
["16", "createBrand", "Create the brand in the third-party CMS."],
["18", "brand.id", "Pass the brand's ID to the compensation function."],
["27", "deleteBrand", "Delete the brand in the third-party CMS 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/framework/workflows-sdk"
import { SyncBrandToSystemInput } from ".."
import BrandModuleService from "../../../modules/brand/service"
import { BRAND_MODULE } from "../../../modules/brand"
```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { InferTypeOf } from "@medusajs/framework/types"
import { Brand } from "../modules/brand/models/brand"
import { CMS_MODULE } from "../modules/cms"
import CmsModuleService from "../modules/cms/service"
export const createBrandInSystemStep = createStep(
"create-brand-in-system",
async ({ id }: SyncBrandToSystemInput, { container }) => {
const brandModuleService: BrandModuleService = container.resolve(
BRAND_MODULE
)
type SyncBrandToCmsStepInput = {
brand: InferTypeOf<typeof Brand>
}
const brand = await brandModuleService.retrieveBrand(id)
await brandModuleService.client.createBrand(brand)
const syncBrandToCmsStep = createStep(
"sync-brand-to-cms",
async ({ brand }: SyncBrandToCmsStepInput, { container }) => {
const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE)
await cmsModuleService.createBrand(brand)
return new StepResponse(null, brand.id)
},
async (id, { container }) => {
const brandModuleService: BrandModuleService = container.resolve(
BRAND_MODULE
)
if (!id) {
return
}
await brandModuleService.client.deleteBrand(id)
const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE)
await cmsModuleService.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.
You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](../../../basics/medusa-container/page.mdx) and use its `createBrand` method. This method will create the brand in the third-party CMS.
In the step, you use the `createBrand` method of the client to create the brand in the third-party system.
You also pass the brand's ID to the step's compensation function. In this function, you delete the brand in the third-party CMS if an error occurs during the workflow's execution.
In the compensation function, you undo the step's action using the `deleteBrand` method of the client.
<Note>
### Add Step to Workflow
Learn more about compensation functions in [this chapter](../../../advanced-development/workflows/compensation-function/page.mdx).
Finally, add this step to the `syncBrandToSystemWorkflow` in `src/workflows/sync-brand-to-system/index.ts`:
</Note>
```ts title="src/workflows/sync-brand-to-system/index.ts"
### Create Workflow
You can now create the workflow that uses the above step. Add the workflow to the same `src/workflows/sync-brands-to-cms.ts` file:
export const syncWorkflowHighlights = [
["19", "useQueryGraphStep", "Retrieve the brand's details."],
["23", "id", "Filter by the brand's ID."],
["26", "throwIfKeyNotFound", "Throw an error if a brand with the specified ID doesn't exist."],
["30", "syncBrandToCmsStep", "Create the brand in the third-party CMS."]
]
```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncWorkflowHighlights}
// other imports...
import { createBrandInSystemStep } from "./steps/create-brand-in-system"
import {
// ...
createWorkflow,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
// ...
export const syncBrandToSystemWorkflow = createWorkflow(
"sync-brand-to-system",
(input: SyncBrandToSystemInput) => {
createBrandInSystemStep(input)
type SyncBrandToCmsWorkflowInput = {
id: string
}
return new WorkflowResponse(undefined)
export const syncBrandToCmsWorkflow = createWorkflow(
"sync-brand-to-cms",
(input: SyncBrandToCmsWorkflowInput) => {
// @ts-ignore
const { data: brands } = useQueryGraphStep({
entity: "brand",
fields: ["*"],
filters: {
id: input.id,
},
options: {
throwIfKeyNotFound: true,
},
})
syncBrandToCmsStep({
brand: brands[0],
} as SyncBrandToCmsStepInput)
return new WorkflowResponse({})
}
)
```
The workflow now calls the step and returns an `undefined` result.
You create a `syncBrandToCmsWorkflow` that accepts the brand's ID as input. The workflow has the following steps:
- `useQueryGraphStep`: Retrieve the brand's details using Query. You pass the brand's ID as a filter, and set the `throwIfKeyNotFound` option to true so that the step throws an error if a brand with the specified ID doesn't exist.
- `syncBrandToCmsStep`: Create the brand in the third-party CMS.
You'll execute this workflow in the subscriber next.
<Note>
Learn more about `useQueryGraphStep` in [this reference](!resources!/references/helper-steps/useQueryGraphStep).
</Note>
---
## 3. Handle brand.created Event
To handle the `brand.created` event, create a subscriber at `src/subscribers/brand-created.ts` with the following content:
You now have a workflow with the logic to sync a brand to the CMS. You need to execute this workflow whenever the `brand.created` event is emitted. So, you'll create a subscriber that listens to and handle the event.
```ts title="src/subscribers/brand-created.ts"
Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/brand-created.ts` with the following content:
![Directory structure of the Medusa application after adding the subscriber](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493774/Medusa%20Book/cms-dir-overview-5_iqqwvg.jpg)
export const subscriberHighlights = [
["7", "brandCreatedHandler", "The function to execute when the event is emitted."],
["8", "data", "The event's data payload."],
["9", "container", "The Medusa container used to resolve resources."],
["10", "id: string", "The expected data payload's type."],
["11", "syncBrandToCmsWorkflow", "Execute the workflow to sync the brand to the CMS."],
["16", "config", "Export the subscriber's configurations."],
["17", "event", "The event that the subscriber is listening to."]
]
```ts title="src/subscribers/brand-created.ts" highlights={subscriberHighlights}
import type {
SubscriberConfig,
SubscriberArgs,
} from "@medusajs/framework"
import { syncBrandToSystemWorkflow } from "../workflows/sync-brand-to-system"
import { syncBrandToCmsWorkflow } from "../workflows/sync-brands-to-cms"
export default async function brandCreatedHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
await syncBrandToSystemWorkflow(container).run({
await syncBrandToCmsWorkflow(container).run({
input: data,
})
}
@@ -187,27 +256,84 @@ export const config: SubscriberConfig = {
}
```
The subscriber handler accesses the event payload in the `event.data` property of its object parameter.
A subscriber file must export:
- The asynchronous function that's executed when the event is emitted. This must be the file's default export.
- An object that holds the subscriber's configurations. It has an `event` property that indicates the name of the event that the subscriber is listening to.
The subscriber function accepts an object parameter that has two properties:
- `event`: An object of event details. Its `data` property holds the event's data payload, which is the brand's ID.
- `container`: The Medusa container used to resolve framework and commerce tools.
In the function, you execute the `syncBrandToCmsWorkflow`, passing it the data payload as an input. So, everytime a brand is created, Medusa will execute this function, which in turn executes the workflow to sync the brand to the CMS.
<Note>
Learn more about subscribers [in this guide](../../../basics/events-and-subscribers/page.mdx).
Learn more about subscribers in [this chapter](../../../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).
To test the subscriber and workflow out, you'll use the [Create Brand API route](../../custom-features/api-route/page.mdx) you created in a previous chapter.
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.
First, start the Medusa application:
```bash npm2yarn
npm run dev
```
Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. So, to retrieve an authenticated token of your admin user, send 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 admin user's credentials.
<Note title="Tip">
Don't have an admin user? Refer to [this guide](../../../installation/page.mdx#create-medusa-admin-user).
</Note>
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 request returns the created brand. 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:
```plain
info: Processing brand.created which has 1 subscribers
http: POST /admin/brands ← - (200) - 16.418 ms
info: Sending a POST request to /brands.
info: Request Data: {
"id": "01JEDWENYD361P664WRQPMC3J8",
"name": "Acme",
"created_at": "2024-12-06T11:42:32.909Z",
"updated_at": "2024-12-06T11:42:32.909Z",
"deleted_at": null
}
info: API Key: "123"
```
---
## 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.
## Next Chapter: Sync Brand from Third-Party CMS to Medusa
You can also automate syncing data from a third-party system to Medusa at a regular interval. In the next chapter, you'll learn how to sync brands from the third-party CMS to Medusa once a day.