docs: revise last chapters of customizations (#10480)
This commit is contained in:
@@ -1,87 +1,106 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Schedule Syncing Brands from Third-Party System`,
|
||||
title: `${pageNumber} Guide: Schedule Syncing Brands from CMS`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
In the previous chapters, you've [integrated a third-party CMS](../service/page.mdx) and implemented the logic to [sync created brands](../handle-event/page.mdx) from Medusa to the CMS.
|
||||
|
||||
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).
|
||||
However, when you integrate a third-party system, you want the data to be in sync between the Medusa application and the system. One way to do so is by automatically syncing the data once a day.
|
||||
|
||||
You can create an action to be automatically executed at a specified interval using scheduled jobs. A scheduled job is an asynchronous function with a specified schedule of when the Medusa application should run it. Scheduled jobs are useful to automate repeated tasks.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about scheduled jobs in [this chapter](../../../basics/scheduled-jobs/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## 1. Implement Syncing Workflow
|
||||
In this chapter, you'll create a scheduled job that triggers syncing the brands from the third-party CMS to Medusa once a day. You'll implement the syncing logic in a workflow, and execute that workflow in the scheduled job.
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module",
|
||||
link: "/learn/customization/custom-features/module"
|
||||
},
|
||||
text: "CMS Module",
|
||||
link: "/learn/customization/integrate-systems/service"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
Start by defining the workflow that syncs the brand from the third-party system.
|
||||
---
|
||||
|
||||
The workflow has the following steps:
|
||||
## 1. Implement Syncing Workflow
|
||||
|
||||
1. Retrieve brands from the third-party system.
|
||||
2. Create new brands in Medusa.
|
||||
3. Update existing brands in Medusa.
|
||||
You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job.
|
||||
|
||||
### Retrieve Brands Step
|
||||
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.
|
||||
|
||||
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:
|
||||
<Note>
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/steps/retrieve-brands-from-system.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
Learn more about workflows in [this chapter](../../../basics/workflows/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
This workflow will have three steps:
|
||||
|
||||
1. `retrieveBrandsFromCmsStep` to retrieve the brands from the CMS.
|
||||
2. `createBrandsStep` to create the brands retrieved in the first step that don't exist in Medusa.
|
||||
3. `updateBrandsStep` to update the brands retrieved in the first step that exist in Medusa.
|
||||
|
||||
### retrieveBrandsFromCmsStep
|
||||
|
||||
To create the step that retrieves the brands from the third-party CMS, create the file `src/workflows/sync-brands-from-cms.ts` with the following content:
|
||||
|
||||

|
||||
|
||||
```ts title="src/workflows/sync-brands-from-cms.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
import CmsModuleService from "../modules/cms/service"
|
||||
import { CMS_MODULE } from "../modules/cms"
|
||||
|
||||
export const retrieveBrandsFromSystemStep = createStep(
|
||||
"retrieve-brands-from-system",
|
||||
const retrieveBrandsFromCmsStep = createStep(
|
||||
"retrieve-brands-from-cms",
|
||||
async (_, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
const cmsModuleService: CmsModuleService = container.resolve(
|
||||
CMS_MODULE
|
||||
)
|
||||
|
||||
const brands = await brandModuleService.client.retrieveBrands()
|
||||
const brands = await cmsModuleService.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.
|
||||
You create a `retrieveBrandsFromSystemStep` that resolves the CMS Module's service and uses its `retrieveBrands` method to retrieve the brands in the CMS. You return those brands in the step's response.
|
||||
|
||||
The step returns the retrieved brands.
|
||||
### createBrandsStep
|
||||
|
||||
### 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`:
|
||||
The brands retrieved in the first step may have brands that don't exist in Medusa. So, you'll create a step that creates those brands. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file:
|
||||
|
||||
export const createBrandsHighlights = [
|
||||
["21", "createBrands", "Create the brands in Medusa"],
|
||||
["30", "deleteBrands", "Delete the brands from Medusa"]
|
||||
["22", "createBrands", "Create the brands in Medusa"],
|
||||
["35", "deleteBrands", "Delete the brands from Medusa"]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/steps/create-brands.ts" highlights={createBrandsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { InferTypeOf } from "@medusajs/framework/types"
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
import { Brand } from "../../../modules/brand/models/brand"
|
||||
```ts title="src/workflows/sync-brands-from-cms.ts" highlights={createBrandsHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
|
||||
// other imports...
|
||||
import BrandModuleService from "../modules/brand/service"
|
||||
import { BRAND_MODULE } from "../modules/brand"
|
||||
|
||||
// ...
|
||||
|
||||
type CreateBrand = {
|
||||
name: string
|
||||
}
|
||||
|
||||
type CreateBrandsInput = {
|
||||
brands: InferTypeOf<typeof Brand>[]
|
||||
brands: CreateBrand[]
|
||||
}
|
||||
|
||||
export const createBrandsStep = createStep(
|
||||
@@ -93,52 +112,52 @@ export const createBrandsStep = createStep(
|
||||
|
||||
const brands = await brandModuleService.createBrands(input.brands)
|
||||
|
||||
return new StepResponse(brands, brands.map((brand) => brand.id))
|
||||
return new StepResponse(brands, brands)
|
||||
},
|
||||
async (ids: string[], { container }) => {
|
||||
async (brands, { container }) => {
|
||||
if (!brands) {
|
||||
return
|
||||
}
|
||||
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
await brandModuleService.deleteBrands(ids)
|
||||
await brandModuleService.deleteBrands(brands.map((brand) => brand.id))
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This step receives the brands to create as input.
|
||||
The `createBrandsStep` accepts the brands to create as an input. It resolves the [Brand Module](../../custom-features/module/page.mdx)'s service and uses the generated `createBrands` method to create the brands.
|
||||
|
||||
<Note title="Tip">
|
||||
The step passes the created brands to the compensation function, which deletes those brands if an error occurs during the workflow's execution.
|
||||
|
||||
Since a data model is a variable, use the `InferTypeOf` utility imported from `@medusajs/framework/types` to infer its type.
|
||||
<Note>
|
||||
|
||||
Learn more about compensation functions in [this chapter](../../../advanced-development/workflows/compensation-function/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
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:
|
||||
The brands retrieved in the first step may also have brands that exist in Medusa. So, you'll create a step that updates their details to match that of the CMS. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file:
|
||||
|
||||
export const updateBrandsHighlights = [
|
||||
["21", "prevUpdatedBrands", "Retrieve the data of the brands before the update."],
|
||||
["25", "updateBrands", "Update the brands in Medusa."],
|
||||
["34", "updateBrands", "Revert the update by reverting the brands' to before the update."]
|
||||
["19", "prevUpdatedBrands", "Retrieve the data of the brands before the update."],
|
||||
["23", "updateBrands", "Update the brands in Medusa."],
|
||||
["36", "updateBrands", "Revert the update by reverting the brands' details to before the update."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/steps/update-brands.ts" highlights={updateBrandsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { InferTypeOf } from "@medusajs/framework/types"
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
import { Brand } from "../../../modules/brand/models/brand"
|
||||
```ts title="src/workflows/sync-brands-from-cms.ts" highlights={updateBrandsHighlights}
|
||||
// ...
|
||||
|
||||
type UpdateBrand = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
type UpdateBrandsInput = {
|
||||
brands: InferTypeOf<typeof Brand>[]
|
||||
brands: UpdateBrand[]
|
||||
}
|
||||
|
||||
export const updateBrandsStep = createStep(
|
||||
@@ -157,6 +176,10 @@ export const updateBrandsStep = createStep(
|
||||
return new StepResponse(updatedBrands, prevUpdatedBrands)
|
||||
},
|
||||
async (prevUpdatedBrands, { container }) => {
|
||||
if (!prevUpdatedBrands) {
|
||||
return
|
||||
}
|
||||
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
@@ -166,27 +189,24 @@ export const updateBrandsStep = createStep(
|
||||
)
|
||||
```
|
||||
|
||||
This step receives the brands to update as input.
|
||||
The `updateBrandsStep` receives the brands to update in Medusa. In the step, you retrieve the brand's details in Medusa before the update to pass them to the compensation function. You then update the brands using the Brand Module's `updateBrands` generated method.
|
||||
|
||||
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.
|
||||
In the compensation function, which receives the brand's old data, you revert the update using the same `updateBrands` method.
|
||||
|
||||
### Create Workflow
|
||||
|
||||
Finally, create the workflow in the file `src/workflows/sync-brands-from-system/index.ts` with the following content:
|
||||
Finally, you'll create the workflow that uses the above steps to sync the brands from the CMS to Medusa. Add to the same `src/workflows/sync-brands-from-cms.ts` file the following:
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/index.ts"
|
||||
```ts title="src/workflows/sync-brands-from-cms.ts"
|
||||
// other imports...
|
||||
import {
|
||||
// ...
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
transform,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { InferTypeOf } from "@medusajs/framework/types"
|
||||
import { retrieveBrandsFromSystemStep } from "./steps/retrieve-brands-from-system"
|
||||
import { createBrandsStep } from "./steps/create-brands"
|
||||
import { updateBrandsStep } from "./steps/update-brands"
|
||||
import { Brand } from "../../modules/brand/models/brand"
|
||||
|
||||
// ...
|
||||
|
||||
export const syncBrandsFromSystemWorkflow = createWorkflow(
|
||||
"sync-brands-from-system",
|
||||
@@ -198,35 +218,37 @@ export const syncBrandsFromSystemWorkflow = createWorkflow(
|
||||
)
|
||||
```
|
||||
|
||||
For now, you only add the `retrieveBrandsFromSystemStep` to the workflow that retrieves the brands from the third-party system.
|
||||
In the workflow, you only use the `retrieveBrandsFromSystemStep` for now, which retrieves the brands from the third-party CMS.
|
||||
|
||||
### 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 values to perform data manipulation directly. Instead, use [transform](../../../advanced-development/workflows/variable-manipulation/page.mdx) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values.
|
||||
|
||||
Next, you need to identify which brands must be created or updated.
|
||||
<Note>
|
||||
|
||||
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.
|
||||
Learn more about data manipulation using `transform` in [this chapter](../../../advanced-development/workflows/variable-manipulation/page.mdx).
|
||||
|
||||
Instead, use the `transform` utility function imported from `@medusajs/framework/workflows-sdk`, which gives you access to the real-time values of the data to perform actions on them.
|
||||
</Note>
|
||||
|
||||
So, replace the `TODO` with the following:
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/index.ts"
|
||||
```ts title="src/workflows/sync-brands-from-cms.ts"
|
||||
const { toCreate, toUpdate } = transform(
|
||||
{
|
||||
brands,
|
||||
},
|
||||
(data) => {
|
||||
const toCreate: InferTypeOf<typeof Brand>[] = []
|
||||
const toUpdate: InferTypeOf<typeof Brand>[] = []
|
||||
const toCreate: CreateBrand[] = []
|
||||
const toUpdate: UpdateBrand[] = []
|
||||
|
||||
data.brands.forEach((brand) => {
|
||||
if (brand.external_id) {
|
||||
toUpdate.push({
|
||||
...brand,
|
||||
id: brand.external_id,
|
||||
id: brand.external_id as string,
|
||||
name: brand.name as string,
|
||||
})
|
||||
} else {
|
||||
toCreate.push(brand)
|
||||
toCreate.push({
|
||||
name: brand.name as string,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -242,19 +264,11 @@ const { toCreate, toUpdate } = transform(
|
||||
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.
|
||||
In `transform`'s function, you loop over the brands array to check which should be created or updated. This logic assumes that a brand in the CMS has an `external_id` property whose value is the brand's ID in Medusa.
|
||||
|
||||
<Note title="Tip">
|
||||
You now have the list of brands to create and update. So, replace the new `TODO` with the following:
|
||||
|
||||
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"
|
||||
```ts title="src/workflows/sync-brands-from-cms.ts"
|
||||
const created = createBrandsStep({ brands: toCreate })
|
||||
const updated = updateBrandsStep({ brands: toUpdate })
|
||||
|
||||
@@ -264,24 +278,28 @@ return new WorkflowResponse({
|
||||
})
|
||||
```
|
||||
|
||||
You pass the brands to be created to the `createBrandsStep`, and the brands to be updated to the `updateBrandsStep`.
|
||||
You first run the `createBrandsStep` to create the brands that don't exist in Medusa, then the `updateBrandsStep` to update the brands that exist in Medusa. You pass the arrays returned by `transform` as the inputs for the steps.
|
||||
|
||||
Then, you return the created and updated brands.
|
||||
Finally, you return an object of the created and updated brands. You'll execute this workflow in the scheduled job next.
|
||||
|
||||
---
|
||||
|
||||
## 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`:
|
||||
You now have the workflow to sync the brands from the CMS to Medusa. Next, you'll create a scheduled job that runs this workflow once a day to ensure the data between Medusa and the CMS are always in sync.
|
||||
|
||||
A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. So, create the file `src/jobs/sync-brands-from-cms.ts` with the following content:
|
||||
|
||||

|
||||
|
||||
```ts title="src/jobs/sync-brands-from-system.ts"
|
||||
import { MedusaContainer } from "@medusajs/framework/types"
|
||||
import { syncBrandsFromSystemWorkflow } from "../workflows/sync-brands-from-system"
|
||||
import { syncBrandsFromCmsWorkflow } from "../workflows/sync-brands-from-cms"
|
||||
|
||||
export default async function (container: MedusaContainer) {
|
||||
const logger = container.resolve("logger")
|
||||
|
||||
const { result } = await syncBrandsFromSystemWorkflow(container).run()
|
||||
const { result } = await syncBrandsFromCmsWorkflow(container).run()
|
||||
|
||||
logger.info(
|
||||
`Synced brands from third-party system: ${
|
||||
@@ -291,31 +309,37 @@ export default async function (container: MedusaContainer) {
|
||||
|
||||
export const config = {
|
||||
name: "sync-brands-from-system",
|
||||
schedule: "* * * * *",
|
||||
schedule: "0 0 * * *", // change to * * * * * for debugging
|
||||
}
|
||||
```
|
||||
|
||||
This defines a scheduled job that runs every minute (for testing purposes).
|
||||
A scheduled job file must export:
|
||||
|
||||
<Note>
|
||||
- An asynchronous function that will be executed at the specified schedule. This function must be the file's default export.
|
||||
- An object of scheduled jobs configuration. It has two properties:
|
||||
- `name`: A unique name for the scheduled job.
|
||||
- `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job.
|
||||
|
||||
Learn more about scheduled jobs [in this guide](../../../basics/scheduled-jobs/page.mdx).
|
||||
The scheduled job function accepts as a parameter the [Medusa container](../../../basics/medusa-container/page.mdx) used to resolve framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated.
|
||||
|
||||
</Note>
|
||||
|
||||
The scheduled job executes the `syncBrandsFromSystemWorkflow` and prints how many brands were created and updated.
|
||||
Based on the cron expression specified in `config.schedule`, Medusa will run the scheduled job every day at midnight. You can also change it to `* * * * *` to run it every minute for easier debugging.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
To test out the scheduled job, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
If you set the schedule to `* * * * *` for debugging, the scheduled job will run in a minute. You'll see in the logs how many brands were created or updated.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
In the previous chapters, you:
|
||||
By following the previous chapters, you utilized Medusa's framework and orchestration tools to perform and automate tasks that span across systems.
|
||||
|
||||
- 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.
|
||||
With Medusa, you can integrate any service from your commerce ecosystem with ease. You don't have to set up separate applications to manage your different customizations, or worry about data inconsistency across systems. Your efforts only go into implementing the business logic that ties your systems together.
|
||||
|
||||
Reference in New Issue
Block a user