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:
Shahed Nasser
2024-09-18 08:45:29 +00:00
committed by GitHub
parent 56ee4d6aad
commit 0c3e3ea88a
21 changed files with 2277 additions and 1 deletions
@@ -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.
@@ -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.