chore: reorganize docs apps (#7228)

* reorganize docs apps

* add README

* fix directory

* add condition for old docs
This commit is contained in:
Shahed Nasser authored and GitHub committed 2024-05-03 17:36:38 +03:00
1 parent 224ebb2154
commit 4fe28f5a95
6187 files changed
+601447 -598226

No files matched your search

@@ -0,0 +1,82 @@
export const metadata = {
title: `${pageNumber} Create a Batch Job`,
}
# {metadata.title}
In this chapter, you’ll learn how to create and process a batch job
<Note type="check">
The API routes used in this chapter require admin user authentication, and the authentication token is referred to in code snippets as `<API_TOKEN>`. Refer to [this guide](https://docs.medusajs.com/api/admin#authentication) on authenticating admin users.
</Note>
## 1. Start Medusa Application
```bash npm2yarn
npm run dev
```
## 2. Create a Batch Job
Use the [Create Batch Job API Route](https://docs.medusajs.com/api/admin#batch-jobs_postbatchjobs) to create a new batch job with the same type as the batch job strategy.
For example:
```bash
curl -X POST 'http://localhost:9000/admin/batch-jobs' \
-H 'x-medusa-access-token: <API_TOKEN>' \
-H 'Content-Type: application/json' \
--data-raw '{
"type": "custom",
"context": { }
}'
```
In the request body of this API route, you must pass two parameters:
- `type`: The batch job’s type. You use `custom`, which is the type of batch job strategy created in the previous chapter.
- `context`: An object holding any additional details you want to pass to the batch job.
This API route creates a batch job in the database. The Medusa application then triggers the processing of the batch job asynchronously.
## 3. Retrieve Batch Job Status
To check the current status of the batch job, send a request to the [Get a Batch Job API route](https://docs.medusajs.com/api/admin#batch-jobs_getbatchjobsbatchjob):
```bash
curl 'http://localhost:9000/admin/batch-jobs/<BATCH_JOB_ID>' \
-H 'x-medusa-access-token: <API_TOKEN>'
```
Replace `<BATCH_JOB_ID>` with the ID of the batch job returned in the previous step.
This API route returns the batch job object. Among its properties, the `status` property indicates the status of the batch job. If it started processing, the status is `processing`. If it’s completed, the status is `completed`.
## 4. Check Console
Once the batch job’s status is `completed`, check the terminal. You’ll find the following messages:
```bash
info: Processing batch.created which has 1 subscribers
info: Processing batch.pre_processed which has 0 subscribers
info: Processing batch.confirmed which has 1 subscribers
Processing the batch job!
info: Processing batch.processing which has 0 subscribers
info: Processing batch.completed which has 0 subscribers
```
These messages indicate the different stages of the batch job’s lifecycle. Whenever the batch job’s status is changed, an event is triggered to allow batch job strategies to handle them.
When the batch job’s status is `confirmed`, the batch job is processed, which is why the `Processing the batch job!` message is logged.
In the next chapter, you’ll learn more about the batch job’s lifecycle.
---
## Alternative Method: BatchJobService
You can alternatively create and process a batch job using the `BatchJobService`.
Refer to the `BatchJobService` reference to learn how to use the `create` method.
@@ -0,0 +1,56 @@
export const metadata = {
title: `${pageNumber} Batch Job Lifecycles`,
}
# {metadata.title}
In this chapter, you’ll learn about the different lifecycle stages of a batch job based on its status.
## created
When a batch job is created through the API route or using the `BatchJobService`, its status is changed to `created` and the `batch.created` event is emitted.
![A diagram illustrating the created stage](https://res.cloudinary.com/dza7lstvk/image/upload/v1708688547/Medusa%20Book/batch-job-created_gthkwh.jpg)
---
## pre_processed
Once the `batch.created` event is emitted, the associated batch job strategy is resolved and its `preProcessBatchJob` method is executed.
Then, the batch job’s status is changed to `pre_processed` and the `batch.pre_processed` event is triggered.
![A diagram illustrating the pre-processed stage](https://res.cloudinary.com/dza7lstvk/image/upload/v1708524703/Medusa%20Book/batch-job-pre_processed_jx0tvt.jpg)
---
## confirmed
After the batch job’s status is changed to `pre_processed`, it’s confirmed automatically if the job’s `dry_run` attribute is disabled (which is, by default). The batch job’s status is changed to `confirmed` and the `batch.confirmed` event is triggered.
<Note title="Tip">
If you enabled the `dry_run` attribute when you created the batch job, you must confirm it manually either through the [API route](https://docs.medusajs.com/api/admin#batch-jobs_postbatchjobsbatchjobconfirmprocessing) or the `BatchJobService`'s `confirm` method.
</Note>
![A diagram illustrating the confirmed stage](https://res.cloudinary.com/dza7lstvk/image/upload/v1708525219/Medusa%20Book/batch-job-confirmed_e9pe05.jpg)
---
## processing and completed
Once the `batch.confirmed` event is emitted, the batch job’s status is changed to `processing` and the `batch.processing` event is emitted. Then, the batch job strategy’s `process` method is executed.
Once the processing is done, the status of the batch job is changed to `completed` and the `batch.completed` event is emitted.
![A diagram illustrating the processing and completed stages](https://res.cloudinary.com/dza7lstvk/image/upload/v1708526149/Medusa%20Book/batch-job-processing-completed_svniou.jpg)
---
## Other Lifecycle Stages
The following lifecycle stages occur only in specific cases:
- `canceled`: If you cancel a batch job using the [API route](https://docs.medusajs.com/api/admin#batch-jobs_postbatchjobsbatchjobcancel) or `BatchJobService`'s `cancel` method, the batch job’s status is changed to `canceled` and the `batch.canceled` event is emitted.
- `failed`: If an error occurs during any stage, the batch job’s status is changed to `failed` and the `batch.failed` event is emitted.
@@ -0,0 +1,46 @@
export const metadata = {
title: `${pageNumber} Override Batch Job Strategies`,
}
# {metadata.title}
In this chapter, you’ll learn how to override a batch job strategy.
## Overview
Medusa defines batch job strategies for different purposes, such as importing products.
You can override a strategy from the Medusa application’s core to customize its implementation. For example, you can override the product-import strategy to handle a custom file structure or format.
---
## How to Override a Batch Job Strategy
To override a batch job strategy, create a new batch job strategy and set its `batchType` property to the same type as the original batch job strategy.
For example:
```ts title="src/strategies/custom-product-import.ts" highlights={[["7", "", "Use the same type as the original batch job strategy."]]}
import {
AbstractBatchJobStrategy,
BatchJobService,
} from "@medusajs/medusa"
class CustomImportProductStrategy
extends AbstractBatchJobStrategy {
static batchType = "product-import"
// ...
}
export default CustomImportProductStrategy
```
This creates a new batch job strategy and sets its type to `product-import`.
Then, when you create a batch job of the same type, the Medusa application resolves and uses your custom batch job strategy instead of the one defined in Medusa.
<Note title="Tip">
Refer to the `AbstractBatchJobStrategy` reference for a full list of methods you can implement.
</Note>
@@ -0,0 +1,95 @@
export const metadata = {
title: `${pageNumber} Batch Jobs`,
}
# {metadata.title}
In this chapter, you’ll learn what batch jobs are and how to create them.
## What are Batch Jobs and Strategies?
A batch job is a task performed asynchronously and iteratively in the background of your Medusa application. The task's implementation is defined in a batch job strategy.
The Medusa application provides API Routes to create a batch job and tracks its progress. You can also create it using the `BatchJobService`'s `create` method.
When a batch job is created, the associated batch job strategy is used to process it.
---
## How to Create a Batch Job Strategy?
A batch job strategy is a class created in a TypeScript or JavaScript file under the `src/strategies` directory. The class must extend the `AbstractBatchJobStrategy` class from the `@medusajs/medusa` package.
For example, create the file `src/strategies/custom.ts` with the following content:
export const highlights = [
["8", "", "A unique identifier associated with the strategy."],
["9", "", "The type of batch job that this strategy is used for."],
["11", "processJob", "Defines the task to perform when the batch job is processed."],
["14", "buildTemplate", "This method is only useful if your batch job provides a template file or text that users can download."]
]
```ts title="src/strategies/custom.ts" highlights={highlights}
import {
AbstractBatchJobStrategy,
BatchJobService,
} from "@medusajs/medusa"
class CustomJobStrategy extends AbstractBatchJobStrategy {
protected batchJobService_: BatchJobService
static identifier = "custom-strategy"
static batchType = "custom"
async processJob(batchJobId: string): Promise<void> {
console.log("Processing the batch job!")
}
async buildTemplate(): Promise<string> {
return ""
}
}
export default CustomJobStrategy
```
This creates a batch job strategy implementing the required properties and methods.
### Properties
A batch job strategy must implement the following properties:
- `identifier`: A unique identifier associated with the strategy.
- `batchType`: The type of batch job that this strategy is used for. A batch job has a type, and, when it's created, the strategy having that type is used to process the job.
### Methods
A batch job strategy must implement the following methods:
- `processJob`: Defines the task to perform when the batch job is processed.
- `buildTemplate`: This method is only useful if your batch job provides a template file or text that users can download. For example, you can return a template CSV file that showcases the accepted CSV format for product import. If your batch job doesn’t support that, you can return an empty string.
A batch job strategy can also implement methods that run before and after a batch job is processed or when it fails. Refer to the Batch Job Strategy reference for all available methods.
---
## When to Use
<Note title="Use batch jobs when" type="success">
- You’re implementing an asynchronous job that’s triggered manually.
- You're tracking the status of the asynchronous job.
- You're keeping track of all batch job executions, which are stored in the database.
</Note>
<Note title="Don’t use batch jobs if" type="error">
- You want to trigger the asynchronous job automatically. Instead, use scheduled jobs. You can also create the batch job in a scheduled job.
- You want the task to be performed and finished before consecutive tasks. Instead, use a service.
</Note>
---
## Test Batch Job Strategy
To test a batch job strategy, use the Admin API Routes to create a batch job of the same type. This is covered in the next chapter.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,27 @@
export const metadata = {
title: `${pageNumber} Admin Development`,
}
# {metadata.title}
<Note type="soon">
Admin customizations are coming soon.
</Note>
In the next chapters, you'll learn more about possible admin customizations.
You can customize the admin dashboard by:
- Adding new sections to existing pages using Widgets.
- Adding new pages using UI Routes.
- Adding new pages to the Settings section of the admin dashboard using Setting Pages.
---
## Medusa UI Package
Medusa provides a Medusa UI package to facilitate your admin development through ready-made components and ensure a consistent design between your customizations and the dashboard’s design.
Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui) to learn how to install it and use its components.
@@ -0,0 +1,152 @@
export const metadata = {
title: `${pageNumber} Admin Setting Pages`,
}
# {metadata.title}
<Note type="soon">
Admin customizations are coming soon.
</Note>
In this chapter, you’ll learn how to create a setting page in the admin dashboard.
## What is a Setting Page?
A setting page is a React component that adds a new page to the settings panel of the admin dashboard.
For example, you may create a setting page to manage configurations related to your custom functionalities.
---
## How to Create a Setting Page?
A setting page is created in a file named `page.tsx` under the `src/admin/settings` directory. The file’s default export must be the React component, and it must also export a configuration object.
For example, you can create the file `src/admin/settings/custom/page.tsx` with the following content:
```tsx title="src/admin/settings/custom/page.tsx"
import type { SettingConfig } from "@medusajs/admin"
import { ChatBubbleLeftRight } from "@medusajs/icons"
const CustomSettingPage = () => {
return (
<div>
<h1>Custom Setting Page</h1>
</div>
)
}
export const config: SettingConfig = {
card: {
label: "Custom",
description: "Manage your custom settings",
// optional
icon: ChatBubbleLeftRight,
},
}
export default CustomSettingPage
```
The new setting page’s path is the file’s path relative to `src/admin/settings` and prefixed with `/a/settings`. So, the above UI route is a new page added at the path `localhost:7001/a/settings/custom`.
The setting page is shown on the Settings panel as a card. In the exported configuration object, you configure the card’s label, description, and optionally icon. In this example, you use an icon from [Medusa’s UI Icons package](https://docs.medusajs.com/ui/icons/overview).
### Test the Setting Page
To test the setting page, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, open the Settings page of the admin dashboard. You’ll find a new card that links to your setting page.
---
## Using UI Components
Similar to other admin customizations, it’s highly recommended that you use the [Medusa UI package](https://docs.medusajs.com/ui) to match your page’s design with the rest of the Medusa Admin.
For example, you can rewrite the above UI route to the following:
```tsx title="src/admin/settings/custom/page.tsx"
import type { SettingConfig } from "@medusajs/admin"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"
const CustomSettingPage = () => {
return (
<Container>
<Heading level="h1">Custom Setting Page</Heading>
</Container>
)
}
export const config: SettingConfig = {
card: {
label: "Custom",
description: "Manage your custom settings",
icon: ChatBubbleLeftRight,
},
}
export default CustomSettingPage
```
---
## Setting Page Props
A setting page receives a `notify` prop, which is an object having the following properties:
- `success`: a function that shows a success toast message.
- `error`: a function that shows an error toast message.
- `warn`: a function that shows a warning toast message.
- `info`: a function that shows an info toast message.
Each of these functions accepts two parameters: the message’s title and the message’s content.
For example:
```tsx title="src/admin/settings/custom/page.tsx" highlights={[["11", "success", "Show a success toast message on the click of a button."]]}
import type { RouteProps, SettingConfig } from "@medusajs/admin"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container, Heading, Button } from "@medusajs/ui"
const CustomSettingPage = ({ notify }: RouteProps) => {
return (
<Container>
<Heading level="h1">Custom Setting Page</Heading>
<Button
onClick={() =>
notify.success("Success!", "You clicked the button!")
}
className="mt-3"
>
Click me
</Button>
</Container>
)
}
export const config: SettingConfig = {
card: {
label: "Custom",
description: "Manage your custom settings",
icon: ChatBubbleLeftRight,
},
}
export default CustomSettingPage
```
If you click the button on your setting page, a toast message will show with the title and message you specified.
<Note title="Tip">
Admin UI Routes support [Tailwind CSS](https://tailwindcss.com/) out of the box.
</Note>
@@ -0,0 +1,72 @@
export const metadata = {
title: `${pageNumber} Admin Development Tips`,
}
# {metadata.title}
<Note type="soon">
Admin customizations are coming soon.
</Note>
In this chapter, you'll find some tips for your admin development.
## Sending Requests to API Routes
To send requests to your API routes in your admin customizations, it’s highly recommended to use the Medusa React or Medusa JS Client packages.
Check out their references for more details on available hooks or methods and how to use them.
---
## Routing Functionalities
To navigate or link to other pages, or use other routing functionalities, use the [react-router-dom](https://reactrouter.com/en/main) package:
```bash npm2yarn
npm install react-router-dom
```
For example:
```tsx title="src/admin/widgets/product-widget.tsx" highlights={[["18", "Link", "Add a link to another page."]]}
import type {
WidgetConfig,
ProductDetailsWidgetProps,
} from "@medusajs/admin"
import { Container, Heading, Button } from "@medusajs/ui"
import { Link } from "react-router-dom"
// The widget
const ProductWidget = ({
product,
}: ProductDetailsWidgetProps) => {
return (
<Container>
<Heading level="h2">
Product Widget {product.title}
</Heading>
<Button className="mt-3" variant="transparent">
<Link to={"/a/orders"}>View Orders</Link>
</Button>
</Container>
)
}
// The widget's configurations
export const config: WidgetConfig = {
zone: "product.details.after",
}
export default ProductWidget
```
This adds a widget in a product's details page with a link to the Orders page.
<Note title="Learn more">
Refer to [react-router-dom’s documentation](https://reactrouter.com/en/main) for other available components and hooks.
</Note>
@@ -0,0 +1,182 @@
export const metadata = {
title: `${pageNumber} Admin UI Routes`,
}
# {metadata.title}
<Note type="soon">
Admin customizations are coming soon.
</Note>
In this chapter, you’ll learn how to create a UI route in the admin dashboard.
## What is a UI Route?
A UI route is a React Component that adds a custom new page to your admin dashboard. The UI Route can be shown in the sidebar or added as a nested page.
For example, you may add a new page to manage product reviews.
---
## How to Create a UI Route
A UI route is created in a file named `page.tsx` under the `src/admin/routes` directory. The file’s default export must be the UI route’s React component.
For example, create the file `src/admin/routes/custom/page.tsx` with the following content:
```tsx title="src/admin/routes/custom/page.tsx"
const CustomPage = () => {
return <div>This is my custom route</div>
}
export default CustomPage
```
The new page’s path is the file’s path relative to `src/admin/routes` and prefixed with `/a`. So, the above UI route is a new page added at the path `localhost:7001/a/custom`.
### Test the UI Route
To test the UI route, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, after logging into the admin dashboard, open the page `localhost:7001/a/custom` to see your custom page.
---
## Using UI Components
Similar to Widgets, it’s highly recommended that you use the [Medusa UI package](https://docs.medusajs.com/ui) to match your page’s design with the rest of the Medusa Admin.
For example, you can rewrite the above UI route to the following:
```tsx title="src/admin/routes/custom/page.tsx"
import { Container } from "@medusajs/ui"
const CustomPage = () => {
return <Container>This is my custom route</Container>
}
export default CustomPage
```
---
## UI Route Props
A UI Route receives a `notify` prop, which is an object having the following properties:
- `success`: a function that shows a success toast message.
- `error`: a function that shows an error toast message.
- `warn`: a function that shows a warning toast message.
- `info`: a function that shows an info toast message.
Each of these functions accepts two parameters: the message’s title and the message’s content.
For example:
```tsx title="src/admin/routes/custom/page.tsx" highlights={[["10", "success", "Show a success toast message on the click of a button."]]}
import { RouteProps } from "@medusajs/admin"
import { Container, Text, Button } from "@medusajs/ui"
const CustomPage = ({ notify }: RouteProps) => {
return (
<Container>
<Text>This is my custom route</Text>
<Button
onClick={() =>
notify.success("Success!", "You clicked the button!")
}
className="mt-3"
>
Click me
</Button>
</Container>
)
}
export default CustomPage
```
<Note title="Tip">
Admin UI Routes support [Tailwind CSS](https://tailwindcss.com/) out of the box.
</Note>
---
## Show UI Route in the Sidebar
A UI route file can export a configuration object that indicates a new item must be added in the sidebar linking to the new UI route.
The configuration object has the property `link`, which is an object having the following properties:
- `label`: the new sidebar item’s label.
- `icon`: an optional React component that acts as an icon in the sidebar. If not provided, a default icon is used.
For example:
```tsx title="src/admin/routes/custom/page.tsx" highlights={[["21"], ["22"], ["23"], ["24"], ["25"], ["26"]]}
import { RouteConfig, RouteProps } from "@medusajs/admin"
import { Container, Text, Button } from "@medusajs/ui"
import { ChatBubbleLeftRight } from "@medusajs/icons"
const CustomPage = ({ notify }: RouteProps) => {
return (
<Container>
<Text>This is my custom route</Text>
<Button
onClick={() =>
notify.success("Success!", "You clicked the button!")
}
className="mt-3"
>
Click me
</Button>
</Container>
)
}
export const config: RouteConfig = {
link: {
label: "Custom Route",
icon: ChatBubbleLeftRight,
},
}
export default CustomPage
```
This adds a new sidebar item with the label `Custom Route` and an icon from the [Medusa UI Icons package](https://docs.medusajs.com/ui/icons/overview).
---
## Path Parameters
A UI route can accept path parameters if the name of any of the directories in its path is of the format `[param]`. For example, `src/admin/routes/custom/[id]/page.tsx`.
To retrieve the path parameters, install the `react-router-dom` to use its `useParams` hook:
```bash npm2yarn
npm install react-router-dom
```
For example, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content:
```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={[["5", "", "Retrieve the path parameter."], ["7", "{id}", "Show the path parameter."]]}
import { useParams } from "react-router-dom"
import { Container } from "@medusajs/ui"
const CustomPage = () => {
const { id } = useParams()
return <Container>Passed ID: {id}</Container>
}
export default CustomPage
```
@@ -0,0 +1,160 @@
import { Table } from "docs-ui"
export const metadata = {
title: `${pageNumber} Admin Widgets`,
}
# {metadata.title}
<Note type="soon">
Admin customizations are coming soon.
</Note>
In this chapter, you’ll learn more about widgets and how to use them.
## What is an Admin Widget?
Admin widgets are React components you inject into predetermined injection zones in the Medusa Admin dashboard.
For example, you can add a widget on the order details page that shows payment details retrieved from Stripe.
---
## How to Create a Widget?
A widget is created in a file under the `src/admin/widgets` directory. The file’s default export must be the widget, which is the React component, and must also export the widget’s configurations.
For example, create the file `src/admin/widgets/product-widget.tsx` with the following content:
```tsx title="src/admin/widgets/product-widget.tsx" highlights={[["4", "ProductWidget", "The React component of the product widget."], ["14", "", "The zone to inject the widget to."]]}
import type { WidgetConfig } from "@medusajs/admin"
// The widget
const ProductWidget = () => {
return (
<div>
<h2>Product Widget</h2>
</div>
)
}
// The widget's configurations
export const config: WidgetConfig = {
zone: "product.details.after",
}
export default ProductWidget
```
The widget only shows the heading `Product Widget`.
In the exported widget’s configurations, you must specify the zone to inject the widget into. The `zone` property can be a string or an array of strings, each being the name of the injection zone.
In the example above, the widget is injected after a product’s details.
### Test the Widget
To test out the widget, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, open a product’s details page. You’ll find your custom widget at the bottom of the page.
---
## Using UI Components
It’s highly recommended that you use the [Medusa UI package](https://docs.medusajs.com/ui) to match your widget’s design with the rest of the Medusa Admin.
For example, you can rewrite the above component to the following:
```tsx title="src/admin/widgets/product-widget.tsx"
import type { WidgetConfig } from "@medusajs/admin"
import { Container, Heading } from "@medusajs/ui"
const ProductWidget = () => {
return (
<Container>
<Heading level="h2">Product Widget</Heading>
</Container>
)
}
export const config: WidgetConfig = {
zone: "product.details.after",
}
export default ProductWidget
```
---
## Widget Props
A widget receives a `notify` prop, which is an object having the following properties:
- `success`: a function that shows a success toast message.
- `error`: a function that shows an error toast message.
- `warn`: a function that shows a warning toast message.
- `info`: a function that shows an info toast message.
Each of these functions accepts two parameters: the message’s title, and the message’s content.
In addition, some injection zones provide additional props based on the page’s context.
For example, you can rewrite the above widget to the following:
```tsx title="src/admin/widgets/product-widget.tsx" highlights={[["14", "product.title", "Show the product's title."], ["18", "success", "Show a success toast message on the click of a button."]]}
import type {
WidgetConfig,
ProductDetailsWidgetProps,
} from "@medusajs/admin"
import { Container, Heading, Button } from "@medusajs/ui"
const ProductWidget = ({
notify,
product,
}: ProductDetailsWidgetProps) => {
return (
<Container>
<Heading level="h2">
Product Widget {product.title}
</Heading>
<Button
onClick={() =>
notify.success("Success!", "You clicked the button!")
}
className="mt-3"
>
Click me
</Button>
</Container>
)
}
export const config: WidgetConfig = {
zone: "product.details.after",
}
export default ProductWidget
```
Since the widget is in the product’s details page, it receives the product as a prop. The widget now shows the title of the product in the header.
It also shows a button that, when you click, shows a success toast message.
<Note title="Tip">
Admin Widgets support [Tailwind CSS](https://tailwindcss.com/) out of the box.
</Note>
---
## Injection Zone
Refer to [this reference](!resources!/admin-widget-injection-zones) for the full list of injection zones and their props.
@@ -0,0 +1,104 @@
export const metadata = {
title: `${pageNumber} Handling CORS in API Routes`,
}
# {metadata.title}
In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes.
## CORS Overview
Cross-Origin Resource Sharing (CORS) allows only configured origins to access your API Routes. For example, if you allow only origins starting with `http://localhost:7001` to access your Admin API Routes, other origins accessing those routes get a CORS error.
### CORS Configurations
You can configure allowed origins for Store and Admin API Routes using the `store_cors` and `admin_cors` configurations in `medusa-config.js`. Each of these configurations accepts a URL pattern to identify allowed origins.
For example:
```js title="medusa-config.js"
module.exports = {
projectConfig: {
admin_cors: "http://localhost:7001",
store_cors: "http://localhost:8000",
// ...
},
// ...
}
```
This allows the `http://localhost:7001` origin to access the Admin API Routes, and the `http://localhost:8000` origin to access Store API Routes.
---
## CORS in Store and Admin Routes
Medusa applies the CORS middleware with the specified configurations in `medusa-config.js` on all routes starting with `/store` and `/admin`.
To disable the CORS middleware for a route, export a `CORS` variable in the route file with its value set to `false`.
For example:
```ts title="src/api/store/custom/route.ts" highlights={[["15"]]}
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
export const GET = (
req: MedusaRequest,
res: MedusaResponse
) => {
res.json({
message: "[GET] Hello world!",
})
}
export const CORS = false
```
This disables the CORS middleware on API Routes at the path `/store/custom`.
---
## CORS in Custom Routes
If you create a route that doesn’t start with `/store` or `/admin`, you must apply the CORS middleware manually. Otherwise, all requests to your API route cause a CORS error.
You can do that in the exported middlewares configurations in `src/api/middlewares.ts`.
For example:
export const highlights = [["18", "parseCorsOrigins", "A utility function that parses the CORS configurations in `medusa-config.js`"]]
```ts title="src/api/middlewares.ts" highlights={highlights}
import {
ConfigModule,
MiddlewaresConfig,
} from "@medusajs/medusa"
import { parseCorsOrigins } from "medusa-core-utils"
import cors from "cors"
export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/custom*",
middlewares: [
(req, res, next) => {
const configModule: ConfigModule =
req.scope.resolve("configModule")
return cors({
origin: parseCorsOrigins(
configModule.projectConfig.store_cors
),
credentials: true,
})(req, res, next)
},
],
},
],
}
```
This retrieves the configurations exported from `medusa-config.js` and applies the `store_cors` to routes starting with `/custom`.
@@ -0,0 +1,47 @@
export const metadata = {
title: `${pageNumber} HTTP Methods`,
}
# {metadata.title}
In this chapter, you'll learn about how to add new API routes for each HTTP method.
## Handlers of HTTP Methods
You can define a handler function for each HTTP method in a route file. The function’s name is the name of the HTTP method it handles.
Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`.
Creating a route handler function for any of the above HTTP methods exposes a new API route for that method.
For example, create the file `src/api/store/hello-world/route.ts` with the following content:
```ts title="src/api/store/hello-world/route.ts"
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
export const GET = (
req: MedusaRequest,
res: MedusaResponse
) => {
res.json({
message: "[GET] Hello world!",
})
}
export const POST = (
req: MedusaRequest,
res: MedusaResponse
) => {
res.json({
message: "[POST] Hello world!",
})
}
```
This adds two API Routes:
- A `GET` route at `localhost:9000/store/hello-world`.
- A `POST` route at `localhost:9000/store/hello-world`.
@@ -0,0 +1,130 @@
export const metadata = {
title: `${pageNumber} Middlewares`,
}
# {metadata.title}
In this chapter, you’ll learn about middlewares and how to create them.
## What is a Middleware?
A middleware is a function executed when a request is sent to an API Route.
---
## How to Create a Middleware?
Middlewares are defined in the special file `src/api/middleware.ts`. The file must export an object of middleware configurations.
For example:
```ts title="src/api/middleware.ts"
import { MiddlewaresConfig } from "@medusajs/medusa"
export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/store*",
middlewares: [
(req, res, next) => {
console.log("Received a request!")
next()
},
],
},
],
}
```
The middleware configurations object has the property `routes`. Its value is an array of middleware route objects, where each object is a middleware to apply to a route pattern.
In the example above, you define a middleware that logs the message `Received a request!` whenever a request is sent to an API route path starting with `/store`.
<Note>
The `matcher` property can be a string or a regular expression.
</Note>
### Test Middleware
To test the middleware, start the application:
```bash npm2yarn
npm run dev
```
Then, send a request to any API route starting with `/store`, such as `localhost:9000/store/products`:
```bash apiTesting testApiUrl="localhost:9000/store/products" testApiMethod="GET"
curl localhost:9000/store/products
```
Once you send the request, you’ll see the following message in the terminal:
```bash
Received a request!
```
---
## When to Use
<Note type="success" title="Use middlewares when">
- You want to guard API routes by a custom condition.
- You're modifying the request body.
- You're registering custom resources in the Medusa container.
</Note>
---
## Middleware Function Parameters
The middleware function accepts three parameters:
1. A request object of type `MedusaRequest`.
2. A response object of type `MedusaResponse`.
3. A function of type `MedusaNextFunction` that executes the next middleware in the stack.
<Note title="Important">
You must call the `next` function in the middleware. Otherwise, other middlewares and the API route handler won’t execute.
</Note>
---
## Restrict HTTP Methods
In addition to the `matcher` configuration, you can restrict which HTTP methods the middleware is applied to.
The object in the `routes` array accepts the property `method` whose value is either a string or an array of strings. Each string is an allowed HTTP method. If no value is specified, the middlewares are applied to requests of all HTTP methods.
For example:
export const highlights = [["7", "", "Apply the middleware only on `POST` requests"]]
```ts title="src/api/middlewares.ts" highlights={highlights}
import { MiddlewaresConfig } from "@medusajs/medusa"
export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/store*",
method: "POST",
middlewares: [
(req, res, next) => {
console.log("Received a request!")
next()
},
],
},
],
}
```
This applies the middleware only when a `POST` request is sent to an API route path starting with `/store`.
@@ -0,0 +1,146 @@
export const metadata = {
title: `${pageNumber} API Route Parameters`,
}
# {metadata.title}
In this chapter, you’ll learn about path, query, and request body parameters.
## Path Parameters
To define a path parameter for an API route, create a directory as part of the route file’s path. The directory’s name is of the format `[param]`, where `param` is the name of the parameters.
For example, to create an API Route at the path `/message/{id}`, where `{id}` is an ID that can be passed to the route, create the file `src/api/store/hello-world/[id]/route.ts` with the following content:
export const singlePathHighlights = [
["11", "req.params.id", "Access the path parameter `id`"]
]
```ts title="src/api/store/hello-world/[id]/route.ts" highlights={singlePathHighlights}
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
export const GET = (
req: MedusaRequest,
res: MedusaResponse
) => {
res.json({
message: `[GET] Hello ${req.params.id}!`,
})
}
```
You can access the path parameter using the `params` property of the `MedusaRequest` parameter. The `params` property is an object whose keys are the parameter names, and the values are each parameter’s value passed in the route’s path.
### Multiple Path Parameters
Each directory in the route file’s path whose name is of the format `[param]` is considered a path parameter. However, every parameter name must be unique.
For example, you can create an API route at `src/api/store/hello-world/[id]/name/[name]/route.ts`:
export const multiplePathHighlights = [
["11", "req.params.id", "Access the path parameter `id`"],
["11", "req.params.name", "Access the path parameter `name`"]
]
```ts title="src/api/store/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights}
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
export const GET = (
req: MedusaRequest,
res: MedusaResponse
) => {
res.json({
message: `[GET] Hello ${req.params.id} - ${req.params.name}!`,
})
}
```
This API route expects two path parameters: `id` and `name`.
---
## Query Parameters
You can access all query parameters in the `query` property of the `MedusaRequest` parameter.
For example:
export const queryHighlights = [
["11", "req.query.name", "Access the query parameter `name`"],
]
```ts title="src/api/store/hello-world/route.ts" highlights={queryHighlights}
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
res.json({
message: `Hello ${req.query.name}`,
})
}
```
---
## Request Body Parameters
By default, any request sent to your Medusa application with its `Content-Type` header set to `application/json` is parsed into an object and attached to the `MedusaRequest`'s `body` property.
For example:
export const bodyHighlights = [
["15", "req.body.name", "Access the request body parameter `name`"],
]
```ts title="src/api/store/hello-world/route.ts" highlights={bodyHighlights}
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
type HelloWorldReq = {
name: string
}
export const POST = (
req: MedusaRequest<HelloWorldReq>,
res: MedusaResponse
) => {
res.json({
message: `[POST] Hello ${req.body.name}!`,
})
}
```
The `MedusaRequest` type accepts a type argument indicating the expected request body parameters.
In this example, you use the `name` request body parameter to create the message in the returned response.
To test it out, send the following request to your Medusa application:
```bash apiTesting testApiUrl="http://localhost:9000/store/hello-world" testApiMethod="POST" testBodyParams={{ "name": "John" }}
curl -X POST http://localhost:9000/store/hello-world \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "John"
}'
```
This returns the following JSON object:
```json
{
"message": "[POST] Hello John!"
}
```
@@ -0,0 +1,155 @@
export const metadata = {
title: `${pageNumber} Protected Routes`,
}
# {metadata.title}
In this chapter, you’ll learn how to create protected routes.
## Default Protected Routes
A protected route is a route that requires requests to be user-authenticated before performing the route's functionality. Otherwise, the request fails, and the user is prevented access.
Medusa applies an authentication guard on the following routes:
- Routes starting with `/admin` require an authenticated admin user.
- Routes starting with `/store/me` require an authenticated customer.
<Note>
Refer to the API Reference for [Admin](https://docs.medusajs.com/api/admin#authentication) and [Store](https://docs.medusajs.com/api/store#authentication) authentication methods.
</Note>
---
## Authentication Opt-Out
To disable the authentication guard on custom routes under the `/admin` or `/store/me` path prefixes, export an `AUTHENTICATE` variable in the route file with its value set to `false`.
For example:
```ts title="src/api/store/me/custom/route.ts" highlights={[["15"]]} apiTesting testApiUrl="http://localhost:9000/store/me/custom" testApiMethod="GET"
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
res.json({
message: "Hello",
})
}
export const AUTHENTICATE = false
```
Now, any request sent to the `/store/me/custom` API route is allowed, regardless if the customer is authenticated or not.
---
## Access Logged-In Customer
You can access the logged-in customer’s ID in all API routes starting with `/store` using the `user.customer_id` property of the `MedusaRequest` object.
For example:
```ts title="src/api/store/custom/route.ts" highlights={[["16", "", "Access the logged-in customer's ID."]]}
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { ICustomerModuleService } from "@medusajs/types"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const customerService: ICustomerModuleService =
req.scope.resolve(ModuleRegistrationName.CUSTOMER)
const customer = await customerService.retrieve(
req.user.customer_id
)
// ...
}
```
In the route handler, you resolve the `CustomerService`, then use it to retrieve the logged-in customer, if available.
---
## Access Logged-In Admin User
You can access the logged-in admin user’s ID in all API Routes starting with `/admin` using the `user.userId` property of the `MedusaRequest` object.
For example:
```ts title="src/api/admin/custom/route.ts" highlights={[["16", "req.user.userId", "Access the logged-in admin user's ID."]]}
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IUserModuleService } from "@medusajs/types"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const userService: IUserModuleService = req.scope.resolve(
ModuleRegistrationName.USER
)
const user = await userService.retrieve(req.user.userId)
// ...
}
```
In the route handler, you resolve the `UserService`, and then use it to retrieve the logged-in admin user.
---
## Protect Custom API Routes
To protect custom API Routes that don’t start with `/store/me` or `/admin`, apply one of the following middlewares exported by the `@medusajs/medusa` package on your routes:
- `authenticate`: only authenticated admin users can access the API Route. You can access the user's ID in the API Route method handler using the `MedusaRequest` object's `user.userId`.
- `authenticateCustomer`: customer authentication isn’t required, but if a customer is logged in, it attaches their ID to the `MedusaRequest` object's `user.customer_id`.
- `requireCustomerAuthentication`: only authenticated customers can access the API Route. You can access the customer's ID in the API Route method handler using the `MedusaRequest` object's `user.customer_id`.
For example:
export const highlights = [
["11", "authenticate", "Only authenticated admin users can access routes starting with `/custom/admin`"],
["15", "requireCustomerAuthentication", "Only authenticated customers can access routes starting with `/custom/customers`"]
]
```ts title="src/api/middlewares.ts" highlights={highlights}
import {
authenticate,
requireCustomerAuthentication,
type MiddlewaresConfig,
} from "@medusajs/medusa"
export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/custom/admin*",
middlewares: [authenticate()],
},
{
matcher: "/custom/customer*",
middlewares: [requireCustomerAuthentication()],
},
],
}
```
@@ -0,0 +1,66 @@
export const metadata = {
title: `${pageNumber} Request Body Parsers`,
}
# {metadata.title}
In this chapter, you’ll learn how to configure request-body parsing and add new parsers.
## Add a Request-Body Parser
You can parse request bodies of other content types by adding the parser as a middleware to the routes.
For example:
export const parserHighlights = [
["13", "", "Add a parser for the `application/x-www-form-urlencoded` content type."]
]
```ts title="src/api/middlewares.ts" highlights={parserHighlights}
import type {
MiddlewaresConfig,
} from "@medusajs/medusa"
import {
urlencoded,
} from "body-parser"
export const config: MiddlewaresConfig = {
routes: [
{
matcher: "*",
middlewares: [
urlencoded({ extended: true }),
],
},
],
}
```
This adds a parser for the `application/x-www-form-urlencoded` content type and attaches the parsed data to the `MedusaRequest` object’s `body` property.
---
## Parse Webhook Body Parameters
Webhook API Routes may require the `raw` body parser middleware rather than the default `json`.
To change the default parser, set the `bodyParser` property of a middleware route object to `false`, and pass the preferred body-parser middleware in the `middlewares` property.
For example:
```ts title="src/api/middlewares.ts" highlights={[["8", "", "Disables the default request-body parser."], ["9", "raw", "Add a new body parser."]]}
import { MiddlewaresConfig } from "@medusajs/medusa"
import { raw } from "body-parser"
export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/webhooks*",
bodyParser: false,
middlewares: [raw({ type: "application/json" })],
},
],
}
```
This changes the request-body parser to use the `raw` middleware on routes starting with `/webhooks`.
@@ -0,0 +1,102 @@
export const metadata = {
title: `${pageNumber} Common Data Model Definitions`,
}
# {metadata.title}
In this chapter, you'll learn where to find resources on common data model definitions related to field's column types, relations, and indices.
## MikroORM Learning Resources
Refer to the following resources to learn about common definitions:
1. [Field column types](https://mikro-orm.io/docs/defining-entities).
2. [Creating indices](https://mikro-orm.io/docs/defining-entities#indexes).
Refer to MikroORM's documentation for more details related to your use case.
---
## Relations Between Data Models
You can build relations between data models in the same module.
Refer to [MikroORM's relations definition](https://mikro-orm.io/docs/relationships)
<Note>
For building relationships between data models in different module, refer to the [Module Relationships chapter](../../modules/module-relationships/page.mdx) instead.
</Note>
---
## Example Data Model
The following example showcase a data model with common definitions:
```ts
import {
BeforeCreate,
Entity,
Enum,
OneToOne,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import {
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/utils"
import ProductVariant from "./product-variant"
export enum MediaType {
MAIN = "main",
PREVIEW = "preview"
}
const VariantIdIndex = createPsqlIndexStatementHelper({
name: "IDX_product_media_variant_id",
tableName: "product_media",
columns: "variant_id",
}).MikroORMIndex
@Entity({ tableName: "product_media" })
export default class ProductMedia {
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "text" })
name: string
@Enum({ items: ["main", "preview"] })
type: MediaType
@Property({ columnType: "text" })
file_key: string
@Property({ columnType: "text" })
mime_type: string
@VariantIdIndex()
@Property({ columnType: "text" })
variant_id: string
@OneToOne({
entity: ProductVariant,
onDelete: "cascade",
})
variant: ProductVariant
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "promed")
}
}
```
In the example above:
- The `ProductMedia` data model has the columns `id`, `name`, `type`, `file_key`, `mime_type`, and `variant_id`.
- The data model has an index on the `variant_id` column.
- The data model has a one-to-one relation to the `ProductVariant` data model.
@@ -0,0 +1,75 @@
export const metadata = {
title: `${pageNumber} Soft-Deletable Models`,
}
# {metadata.title}
In this document, you'll learn how to create soft-deletable data models.
## What is a Soft-Deletable Model?
A soft-deletable data model is a model whose records aren't actually removed from the database when they're deleted.
Instead, their `deleted_at` field is set to the date the record was deleted.
When retrieving or listing records of that data model, records having their `deleted_at` field set aren't retrieved unless the `withDeleted` filter is provided.
---
## How to Create a Soft-Deletable Model?
To create a soft-deletable model, first, add the following filter decorator to the data model class:
```ts title="src/module/hello/models/my-soft-deletable.ts" highlights={[["6"]]}
// other imports...
import { Entity, Filter } from "@mikro-orm/core"
import { DALUtils } from "@medusajs/utils"
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class MySoftDeletable {
// ...
}
```
Then, add a `deleted_at` field to the data model:
```ts highlights={[["4"], ["5"]]}
// other imports...
import { Property } from "@mikro-orm/core"
export default class MySoftDeletable {
// ...
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
}
```
---
## Managing Soft-Deletable Models
Services extending the service factory have methods to soft delete and restore records for all models specified during its creation.
For example, to soft delete a `MySoftDeletable` record:
```ts
await helloModuleService.softDelete([
"id_123", "id_321",
])
```
The method receives an array of IDs of records to delete.
To restore a `MySoftDeletable` record:
```ts
await helloModuleService.restore([
"id_123", "id_321",
])
```
The method also receives an array of IDs of records to restore.
If the data model isn't the main data model, its method names are `softDelete` and `restore` suffixed with the plural name of the model. For example, `softDeleteMySoftDeletable`.
@@ -0,0 +1,44 @@
export const metadata = {
title: `${pageNumber} Event Data Payload`,
}
# {metadata.title}
In this chapter, you'll learn how subscribers receive an event's data payload.
## How to Access Data Payload
When events are emitted, they’re emitted with a data payload.
The object that the subscriber function receives as a parameter has a `data` property, which is the event's data payload.
For example:
export const highlights = [
["8", "", "The event's data payload."],
["9", "{ id: string }", "The type of expected data payloads."]
]
```ts title="src/subscribers/product-created.ts" highlights={highlights}
import {
ProductService,
SubscriberArgs,
type SubscriberConfig,
} from "@medusajs/medusa"
export default async function productCreateHandler({
data,
}: SubscriberArgs<{ id: string }>) {
console.log(`The product ${data.id} was created`)
}
export const config: SubscriberConfig = {
event: ProductService.Events.CREATED,
}
```
This logs the product ID received in the `ProductService.Events.CREATED` (`product-created`) event’s data payload to the console.
## List of Events with Data Payload
Refer to [this reference](!resources!/events-reference) for a full list of events emitted by Medusa and their data payloads.
@@ -0,0 +1,90 @@
export const metadata = {
title: `${pageNumber} Loaders Outside Modules`,
}
# {metadata.title}
In this chapter, you’ll learn how to create a loader outside a module.
## Loaders in the Medusa Application
In your Medusa application, you can create loaders under the `src/loaders` directory. The Medusa application runs these loaders on start-up.
This is useful if you’re performing a task on application start-up, but don’t need to define it in a module.
---
## How to Create a Loader Outside Module?
To create a loader in your Medusa application outside a module, create a TypeScript or JavaScript file under the `src/loaders` directory that default exports a function.
For example, create the file `src/loaders/hello-world.ts` with the following content:
```ts title="src/loaders/hello-world.ts"
export default function () {
console.log(
"[HELLO LOADER] Just started the Medusa application!"
)
}
```
This loader logs the message `[HELLO LOADER] Just started the Medusa application!` when the Medusa application starts.
### Test Out the Loader
To test out your loader, start the Medusa application:
```bash npm2yarn
npm run dev
```
You’ll see in the terminal the logged message, indicating that the loader function was executed successfully.
---
## Resolve Resources
When the loader function is created outside a module, it receives the Medusa container as a parameter. Use it to resolve other resources in your application, such as a module’s service.
For example:
```ts title="src/loaders/hello-world.ts"
import { MedusaContainer } from "@medusajs/medusa"
import { IProductModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
export default async function (container: MedusaContainer) {
const productModuleService: IProductModuleService =
container.resolve(ModuleRegistrationName.PRODUCT)
const [, count] = await productModuleService.listAndCount()
console.log(`Hello! You have ${count} product(s)`)
}
```
In the loader you resolve the `IProductModuleService`, then use its `listAndCount` method to log in the terminal the number of products in the store.
---
## Medusa Configuration Parameter
When the loader function is created outside a module, it receives the Medusa configurations defined in `medusa-config.js` as a second parameter.
For example:
```ts title="src/loaders/hello-world.ts"
import { MedusaContainer } from "@medusajs/medusa"
import { ConfigModule } from "@medusajs/types"
export default async function (
container: MedusaContainer,
config: ConfigModule
) {
console.log(`You have ${
Object.values(config.modules || {}).length
} modules!`)
}
```
This loader logs on application start-up the number of modules defined in your Medusa configurations.
@@ -0,0 +1,56 @@
export const metadata = {
title: `${pageNumber} Database Connection Loader`,
}
# {metadata.title}
In this document, you’ll learn about how to load the database connection in your module.
## Modules Database Connection
By default, a module will use the same database connection of the Medusa application.
To access those database credentials and ensure that MikroORM’s [entity](https://mikro-orm.io/docs/entity-manager) and [transaction](https://mikro-orm.io/docs/transactions) managers use those credentials when performing database operations, you must pass a database connection loader in your module’s definition.
Medusa provides a utility connection loader factory that handles registering the database options and entity and transaction managers into the module’s container.
---
## How to Add a Database Connection Loader?
In your module’s definition file, import the `ModulesSdkUtils` utility function from `@medusajs/utils`. Then, use its `mikroOrmConnectionLoaderFactory` function to create a connection loader and export it in the module’s definition.
For example:
```ts title="src/modules/hello/index.ts"
// other imports...
import { ModulesSdkUtils } from "@medusajs/utils"
import { MyCustom } from "./models/my-custom"
const pathToMigrations = __dirname + "/migrations"
// ...
const connectionLoader = ModulesSdkUtils
.mikroOrmConnectionLoaderFactory({
moduleName,
moduleModels: [MyCustom],
migrationsPath: pathToMigrations,
})
export default {
// ...
loaders: [
// ...
connectionLoader,
],
}
```
The `mikroOrmConnectionLoaderFactory` function accepts an object with the following properties:
- `moduleName`: The name of the module.
- `moduleModules`: An array of data models in the module.
- `migrationsPath`: The path to the migrations file.
You can now implement data-management methods as explained in the next chapters.
@@ -0,0 +1,118 @@
export const metadata = {
title: `${pageNumber} Module's Container`,
}
# {metadata.title}
In this chapter, you'll learn about a module's container and how to register resources in that container.
## Module's Container
Each module has a local container only used by the resources of that module.
So, resources in the module, such as services or loaders, can only resolve resources registered in the module's container.
To register a module's resources in that container, Medusa provides a utility loader factory function that registers your module's services and other resources in the module's container.
---
## How to Register a Modules Resources
In your module's definition file, import the `ModulesSdkUtils` utility functions from the `@medusajs/utils`. Then, use its `moduleContainerLoaderFactory` function to create the container loader and export it in the module's definition.
For example:
```ts title="src/modules/hello/index.ts"
// other imports...
import HelloModuleService from "./service"
import { MyCustom } from "./models/my-custom"
import {
ModulesSdkUtils,
MikroOrmBaseRepository,
} from "@medusajs/utils"
// ...
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
moduleModels: {
MyCustom,
},
moduleRepositories: {
BaseRepository: MikroOrmBaseRepository,
},
moduleServices: {
HelloModuleService,
},
})
export default {
// ...
loaders: [containerLoader],
}
```
### Container Loader Factory Parameters
The `moduleContainerLoaderFactory` function accepts as a parameter an object with the following properties:
- `moduleModels`: An object of the module's data models, where the key is the model's name and the value is the model class.
- `moduleRepositories`: An object of the module's repositories. You import here the `MikroOrmBaseRepository` from `@medusajs/utils` and use it as the value of `BaseRepository`.
- `moduleServices`: An object of the module's services, where the key is the service's registration name and the value is the service class.
### Resources Registered by the Container Loader
The container loader registers in the module's container:
- The services passed in the `moduleServices` property.
- The repositories provided in the `moduleRepositories`.
- A generated service for each data model provided in `moduleModels`.
![Example of registered resources in the container](https://res.cloudinary.com/dza7lstvk/image/upload/v1714400573/Medusa%20Book/modules-container_mkcbaq.jpg)
---
## Resolve Resources
### Services
A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container.
For example, to resolve the `baseRepository` registered by the `containerLoader` in the module's definition:
```ts
import { DAL } from "@medusajs/types"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
}
export default class HelloModuleService {
protected baseRepository_: DAL.RepositoryService
constructor({ baseRepository }: InjectedDependencies) {
this.baseRepository_ = baseRepository
}
// ...
}
```
### Loader
A loader function in a module accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources.
For example:
```ts
import {
LoaderOptions,
} from "@medusajs/modules-sdk"
export default function helloWorldLoader({
container,
}: LoaderOptions) {
const myCustomService = container.resolve("myCustomService")
// ...
}
```
@@ -0,0 +1,122 @@
export const metadata = {
title: `${pageNumber} Database Operations in Service Methods`,
}
# {metadata.title}
In this document, you’ll learn how to implement database operations, such as creating a record, in a service’s methods.
## Transaction Manager
A transaction wraps a set of database operations to ensure that when an error occurs, all database changes made by the executed operations are rolled back.
When you implement a method in your service, you must add the `@InjectTransactionManager` decorator to the method performing database operations. For example:
```ts title="src/modules/hello/service.ts" highlights={[["26"]]}
// other imports
import { InjectTransactionManager } from "@medusajs/utils"
import { DAL } from "@medusajs/types"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
myCustomService: ModulesSdkTypes.InternalModuleService<any>
}
class HelloModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory<
// ...
>(
// ...
) {
protected baseRepository_: DAL.RepositoryService
constructor(
{ baseRepository }: InjectedDependencies
) {
// @ts-ignore
super(...arguments)
this.baseRepository_ = baseRepository
}
@InjectTransactionManager("baseRepository_")
async create(
// TODO add parameters
) {
// TODO add implementation
}
}
```
The `@InjectTransactionManager` decorator accepts as a parameter the name of the service class field to inject the transaction manager to, which is the `baseRepository_` field.
---
## Generated Service Methods
As mentioned in a previous chapter, when you use the container loader factory to register resources in your module’s container, the factory creates a service for each data model you specify and registers it in the container. The registration name is the camel-case name of the data model with `Service` appended to it.
So, the container loader factory creates a `myCustomService` for the `MyCustom` data model.
These generated services already implement data-management methods such as `create` or `update`. So, in your module’s service, you can resolve the generated service and use it to create a record.
For example:
export const highlights = [
["13", "", "Inject myCustomService, which is the service generated by the container loader for the MyCustom data model."],
["23", "", "Add a new field for the generated service of the MyCustom data model."],
["31", "", "Set the class field to the injected dependency."],
["39", "create", "Use the `create` method of the generated service."]
]
```ts title="src/modules/hello/service.ts" highlights={highlights}
// other imports...
import { ModulesSdkTypes } from "@medusajs/types"
import { MyCustom } from "./models/my-custom"
// ...
type CreateMyCustomDTO = {
name: string
}
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
myCustomService: ModulesSdkTypes.InternalModuleService<any>
}
class HelloModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory<
// ...
>(
// ...
) {
protected baseRepository_: DAL.RepositoryService
protected myCustomService_: ModulesSdkTypes.InternalModuleService<MyCustom>
constructor(
{ baseRepository, myCustomService }: InjectedDependencies
) {
// @ts-ignore
super(...arguments)
this.baseRepository_ = baseRepository
this.myCustomService_ = myCustomService
}
@InjectTransactionManager("baseRepository_")
async create(
data: CreateMyCustomDTO,
@MedusaContext() context: Context = {}
): Promise<MyCustomDTO> {
const myCustom = await this.myCustomService_.create(
data,
context
)
return myCustom
}
}
```
In the above example, you add `myCustomService` to the `InjectedDependencies` type to resolve the service in the constructor and set it in a class field.
In the `create` method, you use `myCustomService`'s `create` method to create the record. You pass it the record’s data as a first parameter, and the Medusa application’s context as a second parameter.
@@ -0,0 +1,411 @@
import { Tabs, TabsList, TabsTrigger, TabsContent, TabsContentWrapper, Badge } from "docs-ui"
export const metadata = {
title: `${pageNumber} Example: CRUD Module Service`,
}
# {metadata.title}
In this chapter, you’ll find an example of how to implement CRUD operations in your module’s service and test it out with an API route.
<Note>
The example in this chapter uses the same `hello` module with the `HelloModuleService` and `MyCustom` data model from previous chapters.
</Note>
## 1. Create Types File
Create the file `src/types/hello/my-custom.ts` with the following content:
```ts title="src/types/hello/my-custom.ts"
export type MyCustomDTO = {
id: string
name: string
}
export type CreateMyCustomDTO = {
name: string
}
export type UpdateMyCustomDTO = {
name?: string
}
```
You’ll be using these types in the service.
---
## 2. Create HelloModuleService
Create or change the service at `src/modules/hello/service.ts` with the following content:
```ts title="src/modules/hello/service.ts"
import {
ModulesSdkUtils,
InjectTransactionManager,
MedusaContext,
} from "@medusajs/utils"
import {
DAL,
ModulesSdkTypes,
Context,
} from "@medusajs/types"
import { MyCustom } from "./models/my-custom"
import {
CreateMyCustomDTO,
MyCustomDTO,
UpdateMyCustomDTO,
} from "../../types/hello/my-custom"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
myCustomService: ModulesSdkTypes.InternalModuleService<any>
}
type AllModelsDTO = {
MyCustom: {
dto: MyCustomDTO
}
}
class HelloModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory<
InjectedDependencies,
MyCustomDTO,
AllModelsDTO
>(MyCustom, []) {
protected baseRepository_: DAL.RepositoryService
protected myCustomService_: ModulesSdkTypes.InternalModuleService<MyCustom>
constructor(
{ baseRepository, myCustomService }: InjectedDependencies
) {
// @ts-ignore
super(...arguments)
this.baseRepository_ = baseRepository
this.myCustomService_ = myCustomService
}
@InjectTransactionManager("baseRepository_")
async create(
data: CreateMyCustomDTO,
@MedusaContext() context: Context = {}
): Promise<MyCustomDTO> {
const myCustom = await this.myCustomService_.create(
data,
context
)
return myCustom
}
@InjectTransactionManager("baseRepository_")
async update(
id: string,
data: UpdateMyCustomDTO,
@MedusaContext() context: Context = {}
): Promise<MyCustomDTO> {
const myCustom = await this.myCustomService_.update({
...data,
id,
})
return myCustom
}
}
export default HelloModuleService
```
This creates a `HelloModuleService` that uses the service factory to generate data-management methods like `list`, `retrieve`, and `delete`.
<Note>
Refer to the full list of generated methods in [this chapter](../service-factory/page.mdx#generated-methods).
</Note>
In the service, you also implement the `create` and `update` methods that create and update a `MyCustom` record respectively.
---
## 3. Configure Module Definition
Make sure that the module definition at `src/modules/hello/index.ts` uses the container and connection loaders explained in previous chapters:
```ts title="src/modules/hello/index.ts"
import HelloModuleService from "./service"
import { ModulesSdkUtils, MikroOrmBaseRepository } from "@medusajs/utils"
import { MyCustom } from "./models/my-custom"
const moduleName = "hello"
const pathToMigrations = __dirname + "/migrations"
const migrationScriptOptions = {
moduleName,
models: {
MyCustom,
},
pathToMigrations,
}
export const runMigrations = ModulesSdkUtils
.buildMigrationScript(
migrationScriptOptions
)
export const revertMigration = ModulesSdkUtils
.buildRevertMigrationScript(
migrationScriptOptions
)
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
moduleModels: {
MyCustom,
},
moduleRepositories: {
BaseRepository: MikroOrmBaseRepository,
},
moduleServices: [HelloModuleService],
})
const connectionLoader = ModulesSdkUtils.mikroOrmConnectionLoaderFactory({
moduleName,
moduleModels: [MyCustom],
migrationsPath: pathToMigrations,
})
export default {
service: HelloModuleService,
runMigrations,
revertMigration,
loaders: [containerLoader, connectionLoader],
}
```
<Note>
If you don't have the `MyCustom` data model, refer to [this chapter](../../../basics/data-models/page.mdx).
</Note>
Also, make sure that the module is added in the `modules` object defined in `medusa-config.js`:
```js title="medusa-config.js"
const modules = {
helloModuleService: {
resolve: "./dist/modules/hello",
},
// ...
}
```
---
## 4. Add List and Create API Routes
Create the file `src/api/store/custom/route.ts` with the following content:
```ts title="src/api/store/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
import HelloModuleService from "../../../modules/hello/service"
import { CreateMyCustomDTO } from "../../../types/hello/my-custom"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const helloModuleService: HelloModuleService =
req.scope.resolve(
"helloModuleService"
)
res.json({
my_customs: await helloModuleService.list(),
})
}
type CustomReq = CreateMyCustomDTO
export async function POST(
req: MedusaRequest<CustomReq>,
res: MedusaResponse
): Promise<void> {
const helloModuleService: HelloModuleService =
req.scope.resolve(
"helloModuleService"
)
// skipping validation for simplicity
const myCustom = await helloModuleService.create(req.body)
res.json({
my_custom: myCustom,
})
}
```
This adds two API routes:
1. A `GET` API route at `/store/custom` that retrieves a list of `MyCustom` records.
2. A `POST` API route at `/store/custom` that creates a `MyCustom` record.
---
## 5. Add Retrieve, Update, and Delete API Routes
Create the file `src/api/store/custom/[id]/route.ts` with the following content:
```ts title="src/api/store/custom/[id]/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
import HelloModuleService from "../../../../modules/hello/service"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const helloModuleService: HelloModuleService =
req.scope.resolve(
"helloModuleService"
)
const myCustom = await helloModuleService.retrieve(req.params.id)
res.json({
my_custom: myCustom,
})
}
export async function POST(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const helloModuleService: HelloModuleService =
req.scope.resolve(
"helloModuleService"
)
// skipping validation for simplicity
const myCustom = await helloModuleService.update(
req.params.id,
req.body
)
res.json({
my_custom: myCustom,
})
}
export async function DELETE(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const helloModuleService: HelloModuleService =
req.scope.resolve(
"helloModuleService"
)
await helloModuleService.delete(req.params.id)
res.status(200).json({
success: true,
})
}
```
This creates three API routes:
1. A `GET` API route at `/store/custom/[id]` that retrieves a `MyCustom` record by its ID.
2. A `POST` API route at `/store/custom/[id]` that updates a `MyCustom` record.
3. A `DELETE` API route at `/store/custom/[id]` that deletes a `MyCustom` record.
---
## 6. Test it Out
Start your Medusa application:
```bash npm2yarn
npm run dev
```
Then, test out each of the API routes you created.
<Tabs defaultValue="list-route" layoutType="vertical">
<TabsList>
<TabsTrigger value="list-route">
<Badge variant="green">GET</Badge> /store/custom
</TabsTrigger>
<TabsTrigger value="create-route">
<Badge variant="blue">POST</Badge> /store/custom
</TabsTrigger>
<TabsTrigger value="retrieve-route">
<Badge variant="green">GET</Badge> /store/custom/[id]
</TabsTrigger>
<TabsTrigger value="update-route">
<Badge variant="blue">POST</Badge> /store/custom/[id]
</TabsTrigger>
<TabsTrigger value="delete-route">
<Badge variant="red">DEL</Badge> /store/custom/[id]
</TabsTrigger>
</TabsList>
<TabsContentWrapper>
<TabsContent value="list-route">
```bash apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/custom"
curl http://localhost:9000/store/custom
```
</TabsContent>
<TabsContent value="create-route">
```bash apiTesting testApiMethod="POST" testApiUrl="http://localhost:9000/store/custom" testBodyParams={{ "name": "test" }}
curl -X POST http://localhost:9000/store/custom \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "test"
}'
```
</TabsContent>
<TabsContent value="retrieve-route">
```bash apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/custom/{id}" testPathParams={{ "id": "mc_123" }}
curl http://localhost:9000/store/custom/mc_123
```
</TabsContent>
<TabsContent value="update-route">
```bash apiTesting testApiMethod="POST" testApiUrl="http://localhost:9000/store/custom/{id}" testPathParams={{ "id": "mc_123" }} testBodyParams={{ "name": "test" }}
curl -X POST http://localhost:9000/store/custom/mc_123 \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "test"
}'
```
</TabsContent>
<TabsContent value="delete-route">
```bash apiTesting testApiMethod="DELETE" testApiUrl="http://localhost:9000/store/custom/{id}" testPathParams={{ "id": "mc_123" }}
curl -X DELETE http://localhost:9000/store/custom/mc_123
```
</TabsContent>
</TabsContentWrapper>
</Tabs>
@@ -0,0 +1,431 @@
export const metadata = {
title: `${pageNumber} Example: Module Relationship`,
}
# {metadata.title}
This chapter gives an example of adding a relationship from your custom module to the Product Module.
<Note>
The example in this chapter uses the same `hello` module with the `HelloModuleService` from previous chapters.
</Note>
## Create CustomProductData Data Model
Create the file `src/modules/hello/models/custom-product-data.ts` with the following content:
```ts title="src/modules/hello/models/custom-product-data.ts"
import { generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Entity,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
@Entity()
export class CustomProductData {
@PrimaryKey({ columnType: "text" })
id!: string
@Property({ columnType: "text" })
custom_field: string
@Property({ columnType: "text", nullable: true })
product_id?: string
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "cpd")
}
@OnInit()
OnInit() {
this.id = generateEntityId(this.id, "cpd")
}
}
```
This creates a new data model `CustomProductData` to the `hello` module that stores custom fields related to the `Product` data model in the Product Module
### Add Data Model in MikroORM Configurations
Add your new data model to the MikroORM configurations file at `src/modules/hello/mikro-orm.config.dev.ts`:
```ts title="src/modules/hello/mikro-orm.config.dev.ts"
// other imports...
import { CustomProductData } from "./models/custom-product-data"
module.exports = {
entities: [
// other data models...
CustomProductData,
],
// ...
}
```
### Add Data Model to Module Definition
In your module definition at `src/modules/hello/index.ts`, add the data model to the `models` object:
```ts title="src/modules/hello/index.ts"
// other imports...
import { CustomProductData } from "./models/custom-product-data"
const models = {
// other models...
CustomProductData,
}
// ...
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
moduleModels: models,
// ...
})
const connectionLoader = ModulesSdkUtils.mikroOrmConnectionLoaderFactory({
moduleModels: Object.values(models),
// ...
})
// ...
```
This ensures that the model is passed to the container and connection loader along with other models.
### Add Data Model Alias in Joiner Configurations
Add an alias for the data model in the joiner configurations defined in `src/modules/hello/joiner-config.ts`:
```ts title="src/modules/hello/joiner-config.ts"
// other imports...
import { ModuleJoinerConfig } from "@medusajs/types"
import { CustomProductData } from "./models/custom-product-data"
const joinerConfig: ModuleJoinerConfig = {
// ...,
alias: [
// other aliases
{
name: ["custom_product_data"],
args: {
entity: CustomProductData.name,
methodSuffix: "CustomProductDatas",
},
},
],
}
```
### Create Types
Create the file `src/types/hello/custom-product-data.ts` with the following types:
```ts title="src/types/hello/custom-product-data.ts"
export type CustomProductDataDTO = {
id: string
custom_field: string
product_id?: string
}
export type CreateCustomProductDataDTO = {
custom_field: string
product_id?: string
}
export type UpdateCustomProductDataDTO = {
custom_field?: string
product_id?: string
}
```
You’ll use those in the service next.
### Add Data Model to Main Module Service
In the main module service defined at `src/modules/hello/service.ts`, make the following changes to generate and add methods for the new `CustomProductData` model:
export const serviceHighlights = [
["10", "customProductDataService", "Inject the generated service for the `CustomProductData` data model."],
["16", "CustomProductData", "Specify the expected input/output type of the data model."],
["23", "CustomProductData", "Specify the `CustomProductData` data model to generate methods for it."],
["49", "", "Set the `customProductDataService_` class field to the injected `customProductDataService` service."],
["55", "createCustomProductData", "Add a method that creates a `CustomProductData` record."]
]
```ts title="src/modules/hello/service.ts" highlights={serviceHighlights}
// other imports...
import { CustomProductData } from "./models/custom-product-data"
import {
CreateCustomProductDataDTO,
CustomProductDataDTO,
} from "../../types/hello/custom-product-data"
type InjectedDependencies = {
// other injected dependencies...
customProductDataService:
ModulesSdkTypes.InternalModuleService<any>
}
type AllModelsDTO = {
// other model DTOs...
CustomProductData: {
dto: CustomProductDataDTO
}
}
const generateMethodsFor = [
// other data models generating methods for...
CustomProductData,
]
// ...
class HelloModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory<
InjectedDependencies,
MyCustomDTO,
AllModelsDTO
>(MyCustom, generateMethodsFor) {
// ...
protected customProductDataService_:
ModulesSdkTypes.InternalModuleService<CustomProductData>
constructor(
{
// other injected dependencies...
customProductDataService,
}: InjectedDependencies,
protected readonly moduleDeclaration:
InternalModuleDeclaration
) {
// @ts-ignore
super(...arguments)
// ...
this.customProductDataService_ = customProductDataService
}
// other methods...
@InjectTransactionManager("baseRepository_")
async createCustomProductData(
data: CreateCustomProductDataDTO,
@MedusaContext() context: Context = {}
): Promise<CustomProductDataDTO> {
const customProductData = await this.customProductDataService_.create(
data,
context
)
return this.baseRepository_.serialize<CustomProductDataDTO>(
customProductData
)
}
}
```
In the main service you make the following changes:
1. Add `customProductDataService` to the list of injected dependencies.
2. Add the expected input/output type of `CustomProductData` into the `AllModelsDTO` type, which is passed as the third-argument to the `abstractModuleServiceFactory` function.
3. Add the data model to the `generateMethodsFor` array, which is passed as a second parameter to the `abstractModuleServiceFactory` function.
4. Add a `customProductDataService_` field to the `HelloModuleService` and set its value in the constructor to the injected `customProductDataService`.
5. Create a new method `createCustomProductData` that creates a `CustomProductData` record.
### Generate Migration
Use the following command to generate the migration file:
```bash
npx cross-env MIKRO_ORM_CLI=./src/modules/hello/mikro-orm.config.dev.ts mikro-orm migration:create
```
### Run Migration
Run the following commands to run the latest migration:
```bash npm2yarn
npm run build
npx medusa migrations run
```
---
## Add Relationship
To add the relationship from the `hello` module to the Product Module, make the following change in `src/modules/hello/joiner-config.ts`:
```ts title="src/modules/hello/joiner-config.ts"
// other imports...
import { ModuleJoinerConfig } from "@medusajs/types"
import { Modules } from "@medusajs/modules-sdk"
const joinerConfig: ModuleJoinerConfig = {
// ...
relationships: [
{
serviceName: Modules.PRODUCT,
primaryKey: "id",
foreignKey: "product_id",
alias: "product",
},
],
}
```
This adds a new relationship in the joiner configuration of the `hello` module. The relationship references the `product` alias in the Product Module, which is the alias of the `Product` data model in that module.
The `product_id` field in your module’s data models references the `id` field of the `Product` data model.
---
## Implement Create API Route
In this section, you’ll implement an API route that creates a product (for simplicity) and then creates a `CustomProductData` record that references that product.
<Note title="Good to know">
This example creates a product in a store route to simplify testing the relationship. In a realistic use case, only create products under the `/admin` prefix to ensure that the user is an authenticated admin.
</Note>
Create the file `src/api/store/custom-product-data/route.ts` with the following content:
export const apiRouteHighlights = [
["37", "remoteQuery", "Resolve the remote query function from the Medusa container."],
["44", "", "Create a product using the parameters in the request body."],
["48", "", "Create a `CustomProductData` record with a reference to the created product's ID."],
["54", "remoteQueryObjectFromString", "Create the query to pass to the remote query function."],
["59", '"product.title"', "Retrieve the product title among the `CustomProductData` record's data."],
["60", '"product.handle"', "Retrieve the product handle among the `CustomProductData` record's data."],
["63", "", "Filter fetched records by the ID of the created record."],
["67", "", "Run the query with the remote query function."],
["70", "", "Return the created `CustomProductData` record in the response."]
]
```ts title="src/api/store/custom-product-data/route.ts" highlights={apiRouteHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
import {
CreateCustomProductDataDTO,
} from "../../../types/hello/custom-product-data"
import HelloModuleService from "../../../modules/hello/service"
import type {
IProductModuleService,
CreateProductDTO,
} from "@medusajs/types"
import {
remoteQueryObjectFromString,
ContainerRegistrationKeys,
} from "@medusajs/utils"
import type {
RemoteQueryFunction,
} from "@medusajs/modules-sdk"
type CreateCustomProductDataReq =
CreateCustomProductDataDTO & {
product_data: CreateProductDTO
}
export async function POST(
req: MedusaRequest<CreateCustomProductDataReq>,
res: MedusaResponse
): Promise<void> {
const helloModuleService: HelloModuleService =
req.scope.resolve(
"helloModuleService"
)
const productModuleService: IProductModuleService =
req.scope.resolve(
"productModuleService"
)
const remoteQuery: RemoteQueryFunction = req.scope.resolve(
ContainerRegistrationKeys.REMOTE_QUERY
)
// skipping validation for simplicity
const { product_data: productData, ...rest } = req.body
const product = await productModuleService.create(
productData as CreateProductDTO
)
const customProductData = await helloModuleService
.createCustomProductData({
...rest,
product_id: product.id,
})
const query = remoteQueryObjectFromString({
entryPoint: "custom_product_data",
fields: [
"id",
"custom_field",
"product.title",
"product.handle",
],
variables: {
id: customProductData.id,
},
})
const results = await remoteQuery(query)
res.json({
custom_product_data: results[0],
})
}
```
This API route accepts the necessary request body parameters to create a `CustomProductData` record. It also accepts a `product_data` request body parameter to create the product to be referenced by the `CustomProductData` record.
In the API route handler, you create the product and then create the `CustomProductData` record.
Then, you use the remote query function, resolved from the Medusa container, to fetch the created `CustomProductData` record (filtering by its ID) with the product it references. You can add more product-related fields to the `fields` array passed to `remoteQueryObjectFromString`.
Finally, you return the data returned by the `remoteQuery` function.
### Test Create API Route
To test the API route, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, send a `POST` request to the `/store/custom-product-data` route:
```bash
curl --location 'http://localhost:9000/store/custom-product-data' \
--header 'Content-Type: application/json' \
--data '{
"product_data": {
"title": "Pants"
},
"custom_field": "nice shirt"
}'
```
You’ll receive the following response:
```json
{
"custom_product_data": {
"id": "cpd_01HWWDD260T0CS64WAF8CVHQ9Y",
"custom_field": "nice shirt",
"product_id": "prod_01HWWDD25VPQNNZJ57PFTX2JHE",
"product": {
"title": "Shoes",
"handle": "shoes",
"id": "prod_01HWWDD25VPQNNZJ57PFTX2JHE"
}
}
}
```
@@ -0,0 +1,146 @@
export const metadata = {
title: `${pageNumber} Link Modules`,
}
# {metadata.title}
In this chapter, you’ll learn what a link module is and how to use the remote link in your customizations.
## What is a Link Module?
A link module is a module whose only purpose is to define a relationship between two modules’ data models. The relationship is represented as a pivot or link table in the database, pointing at the primary keys of each data model.
For example, Medusa defines a link module between the Product and Pricing modules. The link module builds a relationship between the `ProductVariant` data model and the `PriceSet` data model.
![Diagram showcasing the link module between the Product and Pricing modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651569/Medusa%20Resources/product-pricing_vlxsiq.jpg)
Link modules provide more flexibility in managing relationships between modules while maintaining module isolation.
Link modules are currently only available for Medusa’s commerce modules. The Medusa application only creates the link tables when both modules are available.
---
## What is the Remote Link?
The remote link is a class with utility methods to manage links between modules. It’s registered in the Medusa container under the `remoteLink` registration name.
For example:
```ts
import {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import {
ModuleRegistrationName,
RemoteLink,
} from "@medusajs/modules-sdk"
import {
ContainerRegistrationKeys,
} from "@medusajs/utils"
export async function POST(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const remoteLink: RemoteLink = req.scope.resolve(
ContainerRegistrationKeys.REMOTE_LINK
)
// ...
}
```
You can use its methods to manage links, such as create or delete links.
### Create Link
To create a link between records of two data models, use the `create` method of the remote link.
For example:
```ts
import { Modules } from "@medusajs/utils"
// ...
await remoteLink.create({
[Modules.PRODUCT]: {
variant_id: product.variants[0].id,
},
[Modules.PRICING]: {
price_set_id: price.id,
},
})
```
The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules.
The value of each module’s property is an object. It defines the values of the linked fields.
So, in the example above, you specify for the Product Module the value of the `variant_id`, and for the Pricing Module the value of `price_set_id`. These are the fields linked between the models of the two modules.
### Dismiss Link
To remove a link between records of two data models, use the `dismiss` method of the remote link. This doesn’t remove the records, only the relation between them.
For example:
```ts
import { Modules } from "@medusajs/utils"
// ...
await remoteLink.dismiss({
[Modules.PRODUCT]: {
variant_id: product.variants[0].id,
},
[Modules.PRICING]: {
price_set_id: price.id,
},
})
```
The `dismiss` method accepts the same parameter type as the create method.
### Cascade Delete Linked Records
If a record, such as a variant, is deleted, use the `delete` method of the remote link to delete all associated links with cascade delete enabled.
For example:
```ts
import { Modules } from "@medusajs/utils"
// ...
await productModuleService.deleteVariants([variant.id])
await remoteLink.delete({
[Modules.PRODUCT]: {
variant_id: variant.id,
},
})
```
This deletes all records linked to the deleted variant with cascade delete enabled in their relationship.
### Restore Linked Records
If a record, such as a variant, that was previously soft-deleted is now restored, use the `restore` method of the remote link to restore all associated links that were cascade deleted.
For example:
```ts
import { Modules } from "@medusajs/utils"
// ...
await productModuleService.restoreVariants([variant.id])
await remoteLink.restore({
[Modules.PRODUCT]: {
variant_id: variant.id,
},
})
```
@@ -0,0 +1,239 @@
import { TypeList, CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `${pageNumber} Module Relationships`,
}
# {metadata.title}
In this document, you’ll learn about creating relationships in queryable modules.
## What is a Relationship in a Queryable Module?
A queryable module can have a relationship to another queryable module in the form of a reference.
The Medusa application resolves these relationships while maintaining isolation between the modules and allowing you to retrieve data across them using the remote query.
<Note title="Use module relationships when" type="success">
- You want to build relationships between the data models of modules.
- You want to assoaciate more fields with the data model of another module.
</Note>
<Note title="Don't use module relationships if" type="error">
- You’re building relationships between data models in the same module. Use foreign keys instead.
</Note>
---
## How to Create a Module Relationship?
<Note title="Tip">
The next chapter provides a detailed example of implementing a relationship between two modules. This section gives the general steps to creating the relationship.
</Note>
Consider you’re creating a data model that adds custom fields associated with a product:
```ts title="src/modules/hello/models/another-custom.ts" highlights={[["13"]]}
import { generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Entity,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
@Entity()
export class CustomProductData {
@PrimaryKey({ columnType: "text" })
id!: string
@Property({ columnType: "text" })
custom_field: string
@Property({ columnType: "text", nullable: true })
product_id?: string
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "cpd")
}
@OnInit()
OnInit() {
this.id = generateEntityId(this.id, "cpd")
}
}
```
The `CustomProductData` data model has a `product_id` field to reference the product it adds custom fields for.
To create a relationship to the `product` data model in the Product Module, add the following to the joiner configuration of your module:
export const relationshipsHighlight = [
["10", "serviceName", "The name of the module that this relationship is referencing."],
["11", "alias", "The alias of the data model you’re referencing in the other module."],
["12", "primaryKey", "The name of the field you’re referencing in the other module’s data model."],
["13", "foreignKey", "The name of the field in your data models referencing the other module’s model."],
]
```ts title="src/modules/hello/joiner-config.ts" highlights={relationshipsHighlight}
// other imports...
import { ModuleJoinerConfig } from "@medusajs/types"
import { Modules } from "@medusajs/modules-sdk"
import { CustomProductData } from "./models/custom-product-data"
const joinerConfig: ModuleJoinerConfig = {
// ...
relationships: [
{
serviceName: Modules.PRODUCT,
alias: "product",
primaryKey: "id",
foreignKey: "product_id",
},
],
}
export default joinerConfig
```
You pass a new property `relationships` to the `joinerConfig`. This property’s value is an array of relationship definitions from your module to other modules.
Each relationship definition object accepts the following properties:
<TypeList types={[
{
name: "serviceName",
type: "`string`",
optional: false,
description: "The name of the module (as added in `medusa-config.js`) that this relationship is referencing. If you’re referencing a Medusa commerce module, use the `Modules` enum imported from `@medusajs/modules-sdk`."
},
{
name: "alias",
type: "`string`",
optional: false,
description: "The alias of the data model you’re referencing in the other module. The alias is defined in the joiner configuration of that module, similar to your module."
},
{
name: "primaryKey",
type: "`string`",
optional: false,
description: "The name of the field you’re referencing in the other module’s data model."
},
{
name: "foreignKey",
type: "`string`",
optional: false,
description: "The name of the field in your data models referencing the other module’s model."
}
]} />
So, the above example creates a relationship to the `Product` data model of the Product Module, which has the alias `product`. `product_id` fields in your module’s data models are considered references to the `id` field of the `Product` data model in the Product Module.
---
## Query Data Across Relationships
The remote query allows you to query data across modules using their relationship without the actual dependency between the modules.
To do that, specify within the `fields` retrieved in the query the referenced data model’s fields. For example:
```ts highlights={[["3", '"product.title"', "Retrieve the referenced product's title."]]}
const query = remoteQueryObjectFromString({
entryPoint: "another_custom",
fields: ["id", "custom_field", "product.title"],
})
const result = await remoteQuery(query)
```
Use `alias`'s value in the relationship definition to reference the other module’s data model. To specify its fields, use dot notation.
So, in the example above, the referenced product’s title is retrieved along with the ID under the `product` property of each record object in the result.
---
## Reference Inner Data Models
If the data model you’re referencing isn’t the main data model of the main module service, pass to the relationship definition the `args` property:
```ts title="src/modules/hello/joiner-config.ts" highlights={[["10", "methodSuffix", "The suffix of the referenced data model's methods."]]}
const joinerConfig: ModuleJoinerConfig = {
// ...
relationships: [
{
serviceName: Modules.PRODUCT,
primaryKey: "id",
foreignKey: "variant_id",
alias: "variant",
args: {
methodSuffix: "Variants",
},
},
],
}
```
The `args` property’s value is an object accepting a `methodSuffix` property. The `methodSuffix` property’s value is the plural name of the data model.
---
## Inverse Relationship
If you’re creating a relationship between two custom modules, you can define the relationship on the referenced side using the `extends` property of the joiner configuration.
For example:
<CodeTabs groupId="inverse-relation">
<CodeTab label="Hello Module" value="hello-module">
```ts title="src/modules/hello/joiner-config.ts"
// Hello module
const joinerConfig: ModuleJoinerConfig = {
// ...
relationships: [
{
serviceName: "anotherHelloModuleService",
alias: "another_custom",
primaryKey: "id",
foreignKey: "another_custom_id",
},
],
}
```
</CodeTab>
<CodeTab label="Another Hello Module" value="another-hello-module">
```ts title="src/modules/another-hello/joiner-config.ts" highlights={[["6", "serviceName", "The name of the module this relationship was originally created in."], ["7", "relationship", "The relationship object as defined in the referencing module."]]}
// Another hello module
const joinerConfig: ModuleJoinerConfig = {
// ...
extends: [
{
serviceName: "helloModuleService",
relationship: {
serviceName: "anotherHelloModuleService",
alias: "another_custom",
primaryKey: "id",
foreignKey: "another_custom_id",
},
},
],
}
```
</CodeTab>
</CodeTabs>
The `extends` property’s value is an array of objects having the following properties:
1. `serviceName`: The name of the module this relationship was originally created in.
2. `relationship`: The relationship object as defined in the referencing module.
@@ -0,0 +1,147 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `${pageNumber} Module Options`,
}
# {metadata.title}
In this chapter, you’ll learn about passing options to your Module from the Medusa application’s configurations and using them in the Module’s resources.
## What are Module Options?
A module can receive options to customize or configure its functionality.
For example, if you’re creating a module that integrates a third-party service, you’ll want to receive the integration credentials in the options rather than adding them directly in your code.
A module can receive options when they’re added to the Medusa application’s configuration.
---
## How to Pass Options to a Module?
To pass options to a module, add an `options` property to the module’s configuration in `medusa-config.js`.
For example:
```js title="medusa-config.js"
const modules = {
helloModuleService: {
resolve: "./dist/modules/hello",
options: {
capitalize: true,
},
},
// ...
}
```
The `options` property’s value is an object. You can pass any properties you want.
---
## Access Module Options in Main Service
The module’s main service receives the module’s declaration as a second parameter. The module declaration is an object having an `options` property. Use it to access the options passed from the Module’s configurations.
For example:
<CodeTabs groupId="service-type">
<CodeTab label="Using Service Factory" value="service-factory">
```ts highlights={[["24"], ["28"], ["29"], ["30"]]}
// other imports...
import { InternalModuleDeclaration } from "@medusajs/types"
// ...
// recommended to define type in another file
type HelloModuleOptions = {
capitalize?: boolean
}
class HelloModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory<
// ...
>(
// ...
) {
// ...
protected options_: HelloModuleOptions
constructor(
{
// ...
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
//...
this.options_ = moduleDeclaration.options || {
capitalize: false,
}
}
}
```
</CodeTab>
<CodeTab label="Without Service Factory" value="no-service-factory">
```ts highlights={[["9"], ["13"], ["14"], ["16"], ["17"], ["18"]]}
import { InternalModuleDeclaration } from "@medusajs/types"
// recommended to define type in another file
type HelloModuleOptions = {
capitalize?: boolean
}
export default class HelloModuleService {
protected options_: HelloModuleOptions
constructor(
{},
protected readonly moduleDeclaration:
InternalModuleDeclaration
) {
this.options_ = moduleDeclaration.options || {
capitalize: false,
}
}
getMessage() {
return "Hello, world!"
}
}
```
</CodeTab>
</CodeTabs>
---
## Access Module Options in Loader
The object that a module’s loaders receive as a parameter has an `options` property. Use it to access the options passed from the Module’s configurations.
For example:
```ts title="src/modules/hello/loaders/hello-world.ts" highlights={[["11"], ["16"]]}
import {
LoaderOptions,
} from "@medusajs/modules-sdk"
// recommended to define type in another file
type HelloModuleOptions = {
capitalize?: boolean
}
export default function helloWorldLoader({
options,
}: LoaderOptions<HelloModuleOptions>) {
console.log(
"[HELLO MODULE] Just started the Medusa application!",
options
)
}
```
@@ -0,0 +1,178 @@
import { TypeList } from "docs-ui"
export const metadata = {
title: `${pageNumber} Queryable Modules`,
}
# {metadata.title}
In this chapter, you’ll learn what a queryable module is and how to configure a module to be queryable.
## What is a Queryable Module?
A queryable module is one whose data can be queried more flexibly and referenced in other modules.
By making a module queryable, you can fetch its data using object and GraphQL syntax without using the main service’s methods.
<Note title="Use queryable modules when" type="success">
- You want more flexibility when fetching data.
- You’re creating relations between modules and querying data across modules.
</Note>
---
## How to Make a Module Queryable
<Note title="Steps Summary">
1. Configure module to be queryable.
2. Add `__joinerConfig` method to main service.
</Note>
### 1. Adjust Module Configuration
To make a module queryable, adjust its configuration object passed to the `modules` object in `medusa-config.js`. The configuration object accepts a `definition` property, whose value is an object of advanced module configurations.
For example:
export const configHighlights = [
["5", "key", "The module's key in the `modules` object."],
["6", "registrationName", "The name that the main service is registered under in the Medusa container. It’s recommended to be the same as `key`'s value."],
["7", "isQueryable", "Whether the module is queryable."]
]
```js title="medusa-config.js" highlights={configHighlights}
const modules = {
helloModuleService: {
resolve: "./dist/modules/hello",
definition: {
key: "helloModuleService",
registrationName: "helloModuleService",
isQueryable: true,
},
},
// ...
}
```
The `definition` property’s value accepts the following properties, among others:
<TypeList types={[
{
name: "key",
type: "`string`",
optional: false,
description: "The module's key in the `modules` object."
},
{
name: "registrationName",
type: "`string`",
optional: false,
description: "The name that the main service is registered under in the Medusa container. It’s recommended to be the same as `key`'s value.",
},
{
name: "isQueryable",
type: "`boolean`",
description: "Whether the module is queryable."
}
]} sectionTitle="Adjust Module Configuration" />
### 2. Define `__joinerConfig` Method
Queryable modules must define a public `__joinerConfig` method in their main service. This method returns an object that defines how a module’s data can be accessed and referenced.
Create the file `src/modules/hello/joiner-config.ts` with the following content:
```ts title="src/modules/hello/joiner-config.ts"
import { ModuleJoinerConfig } from "@medusajs/types"
import { MyCustom } from "./models/my-custom"
const joinerConfig: ModuleJoinerConfig = {
serviceName: "helloModuleService",
primaryKeys: ["id"],
alias: [
{
name: ["my_custom"],
args: {
entity: MyCustom.name,
},
},
],
}
export default joinerConfig
```
The `joinerConfig` object of type `ModuleJoinerConfig` imported from `@medusajs/types` accepts the following properties:
<TypeList types={[
{
name: "serviceName",
type: "`string`",
optional: false,
description: "The name of the main module service as registered in the Medusa container."
},
{
name: "primaryKeys",
type: "`string[]`",
optional: false,
description: "The primary key field names used in the data models of the module.",
},
{
name: "alias",
type: "`object[]`",
description: "The alias definitions for each data model in the module.",
children: [
{
name: "name",
type: "`string[]`",
description: "The alias names used later when fetching or referencing the data in the model."
},
{
name: "args",
type: "`object`",
description: "The alias's arguments.",
children: [
{
name: "entity",
type: "string",
description: "The name of the data model this alias is defined for."
}
]
}
]
}
]} sectionTitle="Define __joinerConfig Method" />
Then, import that file in `src/modules/hello/service.ts` and add a new `__joinerConfig` method:
```ts title="src/modules/hello/service.ts" highlights={[["3"], ["14"], ["15"], ["16"]]}
// other imports...
import { ModuleJoinerConfig } from "@medusajs/types"
import joinerConfig from "./joiner-config"
class HelloModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory<
// ...
>(
// ...
) {
// ...
__joinerConfig(): ModuleJoinerConfig {
return joinerConfig
}
// ...
}
```
---
## Querying a Queryable Module
The next chapter explains how to query the data of a Queryable module.
@@ -0,0 +1,392 @@
import { TypeList, Tabs, TabsList, TabsTrigger, TabsContent, TabsContentWrapper } from "docs-ui"
export const metadata = {
title: `${pageNumber} Remote Query`,
}
# {metadata.title}
In this chapter, you’ll learn about the remote query and how to use it to fetch data from modules.
## What is the Remote Query?
The remote query fetches data across queryable modules. It’s a function registered in the Medusa container under the `remoteQuery` key.
In your resources, such as API routes or workflows, you can resolve the remote query to fetch data across custom modules and Medusa’s commerce modules.
---
## Example: Query Hello Module
For example, create the route `src/api/store/query/route.ts` with the following content:
export const exampleHighlights = [
["18", "", "Resolve the remote query from the Medusa container."],
["21", "remoteQueryObjectFromString", "Utility function to build the query."],
["22", "entryPoint", "The alias name of the model you’re querying."],
["23", "fields", "An array of the data model’s field names to retrieve in the result."],
["27", "remoteQuery", "Run the query using the remote query."]
]
```ts title="src/api/store/query/route.ts" highlights={exampleHighlights}
import {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import {
remoteQueryObjectFromString,
ContainerRegistrationKeys,
} from "@medusajs/utils"
import type {
RemoteQueryFunction,
} from "@medusajs/modules-sdk"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const remoteQuery: RemoteQueryFunction = req.scope.resolve(
ContainerRegistrationKeys.REMOTE_QUERY
)
const query = remoteQueryObjectFromString({
entryPoint: "my_custom",
fields: ["name", "id"],
})
res.json({
my_customs: await remoteQuery(query),
})
}
```
In the above example, you resolve `remoteQuery` from the Medusa container.
Then, you create a query using the `remoteQueryObjectFromString` utility function imported from `@medusajs/utils`. This function accepts as a parameter an object with the following required properties:
- `entryPoint`: The alias name of the model you’re querying. In the previous chapter, you added to the joiner configurations of the module the alias `my_custom` for the `MyCustom` data model.
- `fields`: An array of the data model’s field names to retrieve in the result.
You then pass the query to the `remoteQuery` function to retrieve the results.
### Test API Route
To test out the API route, run the Medusa application:
```bash npm2yarn
npm run dev
```
Then, send a `GET` request to the `/store/query` API route:
```bash apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/query"
curl http://localhost:9000/store/query
```
You’ll receive an array of `MyCustom` records under the `my_customs` key in the JSON response.
---
## Apply Filters
The `remoteQueryObjectFromString` function accepts a `variable` property. You can use this property to filter retrieved records.
For example:
```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const query = remoteQueryObjectFromString({
entryPoint: "my_custom",
fields: ["name", "id"],
variables: {
filters: {
id: "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
},
},
})
const result = await remoteQuery(query)
```
The `variable` property’s value is an object having the property `filters`, whose value is also an object. The object’s properties are the field names, and the values are the filters to apply.
You can also filter by multiple values using array values. For example:
```ts highlights={[["6"], ["7"], ["8"], ["9"]]}
const query = remoteQueryObjectFromString({
entryPoint: "my_custom",
fields: ["name", "id"],
variables: {
filters: {
id: [
"mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
"mc_01HWSVWK3KYHKQEE6QGS2JC3FX",
],
},
},
})
const result = await remoteQuery(query)
```
---
## Sort Records
To sort returned records, pass an `order` property to the `variables` property's value. The `order` property is an object whose keys are field names, and values are either:
- `ASC` to sort records by that field in ascending order.
- `DESC` to sort records by that field in descending order.
For example:
```ts highlights={[["4"], ["5"], ["6"]]}
const query = remoteQueryObjectFromString({
entryPoint: "my_custom",
variables: {
order: {
name: "DESC",
},
},
fields: ["name", "id"],
})
const result = await remoteQuery(query)
```
This retrieves the `MyCustom` records sorted by their name in descending order.
---
## Apply Pagination
To paginate the returned records, pass the following properties to the `variables` property's value:
- `skip`: (required to apply pagination) The number of records to skip before fetching the results.
- `take`: The number of records to fetch.
For example:
```ts highlights={[["4", "", "The number of records to skip before fetching the results."], ["5", "", "The number of records to fetch."]]}
const query = remoteQueryObjectFromString({
entryPoint: "my_custom",
variables: {
skip: 0,
take: 10,
},
fields: ["name", "id"],
})
const {
rows,
metadata: { count, take, skip },
} = await remoteQuery(query)
```
This skips no records and returns the first `10` records.
When the pagination fields are provided, the `remoteQuery` returns an object having two properties:
<TypeList types={[
{
name: "rows",
type: "`array`",
description: "The returned records."
},
{
name: "metadata",
type: "`object`",
description: "The pagination details",
children: [
{
name: "skip",
type: "`number`",
description: "The number of records skipped."
},
{
name: "take",
type: "`number`",
description: "The number of records requested to fetch."
},
{
name: "count",
type: "`number`",
description: "The total number of records."
}
]
}
]} sectionTitle="Apply Pagination" />
---
## Using GraphQL
The remote query function alternatively accepts a string with GraphQL syntax as the query.
For example:
```ts title="src/api/store/query/route.ts" apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/query"
import {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import { remoteQueryObjectFromString } from "@medusajs/utils"
import { ContainerRegistrationKeys } from "@medusajs/utils"
import type {
RemoteQueryFunction,
} from "@medusajs/modules-sdk"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const remoteQuery: RemoteQueryFunction = req.scope.resolve(
ContainerRegistrationKeys.REMOTE_QUERY
)
const query = `
query {
my_custom {
id
name
}
}
`
res.json({
my_customs: await remoteQuery(query),
})
}
```
This runs a GraphQL query to retrieve `MyCustom` records.
### Example Usages
<Tabs defaultValue="filters" layoutType="vertical" className="mt-2">
<TabsList>
<TabsTrigger value="filters">Apply Filters</TabsTrigger>
<TabsTrigger value="sort">Sort Records</TabsTrigger>
<TabsTrigger value="pagination">Apply Pagination</TabsTrigger>
</TabsList>
<TabsContentWrapper>
<TabsContent value="filters" className="[&_h3]:!mt-0">
### Apply Filters with GraphQL
The `remoteQuery` function accepts as a second parameter an object of variables to reference in the GraphQL query.
For example, to filter the items by their ID:
```ts highlights={[["2"], ["3"], ["12"], ["13"], ["14"]]}
const query = `
query($id: ID) {
my_custom(id: $id) {
id
name
}
}
`
const result = await remoteQuery(
query,
{
id: "mc_01HWSVWK3KYHKQEE6QGS2JC3FX"
}
)
```
The variable’s value can also be an array to match multiple items.
</TabsContent>
<TabsContent value="sort" className="[&_h3]:!mt-0">
### Sort Records with GraphQL
To sort the records by a field, pass an `order` argument whose value is an object. The object’s key is the field’s name, and its value is either:
- `ASC` to sort items by that field in ascending order.
- `DESC` to sort items by that field in descending order.
For example:
```ts highlights={[["3"]]}
const query = `
query {
my_custom(order: {name: DESC}) {
id
name
}
}
`
const result = await remoteQuery(query)
```
</TabsContent>
<TabsContent value="pagination" className="[&_h3]:!mt-0">
### Pagination with GraphQL
To paginate the records retrieved, pass a `skip` and `take` records in your query, and pass their values in the second parameter of the `remoteQuery` function.
For example:
```ts highlights={[["2"], ["3"]]}
const query = `
query($skip: Int, $take: Int) {
my_custom(skip: $skip, take: $take) {
id
name
}
}
`
const {
rows,
metadata: { count, take, skip }
} = await remoteQuery(
query,
{
skip: 0,
take: 10
}
)
```
This skips no records and returns the first `10` records.
When the pagination fields are provided, the `remoteQuery` returns an object having two properties:
<TypeList types={[
{
name: "rows",
type: "`array`",
description: "The returned records."
},
{
name: "metadata",
type: "`object`",
description: "The pagination details",
children: [
{
name: "skip",
type: "`number`",
description: "The number of records skipped."
},
{
name: "take",
type: "`number`",
description: "The number of records requested to fetch."
},
{
name: "count",
type: "`number`",
description: "The total number of records."
}
]
}
]} sectionTitle="Pagination with GraphQL" />
</TabsContent>
</TabsContentWrapper>
</Tabs>
@@ -0,0 +1,160 @@
export const metadata = {
title: `${pageNumber} Service Factory`,
}
# {metadata.title}
In this document, you’ll learn about what the service factory is and how to use it to create a service.
## What is the Service Factory?
Medusa provides a service factory that your module’s services can extend. The service factory implements data management methods for the data models you specify.
If your service provides functionalities to manage the records of a data model, it’s encouraged to extend the service factory.
---
## How to Extend the Service Factory?
<Note type="check">
Your module must be using the following loaders from the previous chapters:
- [Container Loader](../container/page.mdx).
- [Connection Loader](../connection-loader/page.mdx).
</Note>
The `ModulesSdkUtils` imported from `@medusajs/utils` has a utility function `abstractModuleServiceFactory` that creates a service instance with basic data management methods.
So, to create a service that extends the service instance returned by the factory, call `abstractModuleServiceFactory` and extend its returned value:
```ts title="src/modules/hello/service.ts"
import { ModulesSdkUtils } from "@medusajs/utils"
import { MyCustom } from "./models/my-custom"
class HelloModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory(
MyCustom, []
) {
// TODO implement custom methods
}
export default HelloModuleService
```
### Function Parameters
The `abstractModuleServiceFactory` function accepts two parameters:
1. The first parameter is the main data model this service is creating methods for. It's the `MyCustom` in the example above.
2. The second parameter is an array of data models to generate methods for. If you have the `AnotherCustom` data model, this is where you add it.
### Generated Methods
The service factory generates the following methods for the main data model:
- `list`: Returns an array of records based on filters and pagination configurations.
- `listAndCount`: Returns a tuple of an array of records and the total count of available records based on the filters and pagination configurations provided.
- `retrieve`: Returns a record by its ID.
- `delete`: Deletes records by an ID or filter.
- `softDelete`: Soft-deletes a record by an ID or filter. This only applies if the data model has a `deleted_at` field.
- `restore`: Restores a soft-deleted record by an ID or filter. This only applies if the data model has a `deleted_at` field.
For other provided data models, the same methods are generated, with the data model’s name appended to the end of the method. For example, `listAnotherCustom` or `retrieveAnotherCustom`.
### Type Arguments
For a better development experience and accurate typing of the generated methods, the `abstractModuleServiceFactory` function accepts three type arguments:
export const typeArgsHighlights = [
["24", "InjectedDependencies", "The type of dependencies resolved from the Module's container."],
["25", "MyCustomDTO", "The expected input/output type of the main data model's generated methods."],
["26", "AllModelsDTO", "The expected input/output type of the generated methods of every data model."],
]
```ts title="src/modules/hello/service.ts" highlights={typeArgsHighlights}
import { ModulesSdkUtils } from "@medusajs/utils"
import { MyCustom } from "./models/my-custom"
type MyCustomDTO = {
id: string
name: string
}
type InjectedDependencies = {
// TODO add dependencies
}
type AllModelsDTO = {
MyCustom: {
dto: MyCustomDTO
}
}
// add other data models in your module here.
const generateMethodsFor = []
class HelloModuleService extends ModulesSdkUtils
.abstractModuleServiceFactory<
InjectedDependencies,
MyCustomDTO,
AllModelsDTO
>(MyCustom, generateMethodsFor) {
// TODO implement custom methods
}
export default HelloModuleService
```
1. The first one is the type of the injected dependencies received as the first parameter of the service’s constructor. You use this type to specify the resources to resolve from the module’s container.
2. The second one is the expected input and output type of the main data model’s methods (for example, the `list` method). This is useful to infer the correct input/output of the generated methods of the main model.
3. The third type is the expected input and output type of all data models that the service factory generates methods for (for example, the `listAnotherCustom` method). This is useful to infer the generated methods for each specified data model, with the correct input/output for each of them.
---
## Test it Out
To test out the service, create an API route at `src/api/store/custom/route.ts` with the following content:
```ts title="src/api/store/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
import HelloModuleService from "../../../modules/hello/service"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const helloModuleService: HelloModuleService =
req.scope.resolve(
"helloModuleService"
)
res.json({
my_customs: await helloModuleService.list(),
})
}
```
Then, start your Medusa application:
```bash npm2yarn
npm run dev
```
Finally, send a request `GET` request to `/store/custom`:
```bash apiTesting testApiUrl="http://localhost:9000/store/custom" testApiMethod="GET"
curl http://localhost:9000/store/custom
```
You’ll receive the following response:
```json
{
"my_customs": []
}
```
In the next chapter, you’ll learn how to add create and update methods to the service generated by the service factory.
@@ -0,0 +1,16 @@
export const metadata = {
title: `${pageNumber} Advanced Development`,
}
# {metadata.title}
In the previous chapters, you got a brief introduction to Medusa’s basic concepts. However, to build a custom commerce application, you need a deeper understanding of how you can utilize these concepts for your business use case.
The next chapters dive deeper into each concept. By the end of these chapters, you’ll be able to:
- Expose API routes with control over authentication, parsing request bodies, and more.
- Create data models with complex fields and relations.
- Manage data models in services.
- Link data models in your module to data models of other modules.
- Emit and handle custom events.
- Create advanced workflows with steps across services and third-party systems.
@@ -0,0 +1,216 @@
export const metadata = {
title: `${pageNumber} Data Management Tips`,
}
# {metadata.title}
This chapter provides some tips when implementing a service that manages a data model.
## Filter Records
Medusa provides a `buildQuery` utility method that accepts filters and returns a query object. Then, the query object can be passed to the Repository’s find and list methods to filter the retrieved methods.
For example:
```ts title="src/services/my-custom.ts" highlights={[["8"], ["14"]]}
// other imports
import {
Selector,
buildQuery,
} from "@medusajs/medusa"
class MyCustomService extends TransactionBaseService {
async list(
selector: Selector<MyCustom>
): Promise<MyCustom[]> {
const myCustomRepo =
this.activeManager_.getRepository(MyCustom)
const query = buildQuery(selector)
return await myCustomRepo.find(query)
}
}
```
The `buildQuery` method accepts a parameter of type `Selector` provided by Medusa. `Selector` accepts the target data model as a type argument.
When implementing a `list` method like the above, you can define a selector parameter that can be passed to the method to apply filters on the retrieved records.
For example, to use this method in another resource:
```ts
const items = await myCustomService.list({
name: "John",
})
```
---
## Paginate Records
The `buildQuery` method accepts an optional second parameter of type `FindConfig`, also provided by Medusa. Like `Selector`, it accepts the target data model as a type argument.
For example, you can change the implementation of the `list` method to the following:
```ts title="src/services/my-custom.ts" highlights={[["7", "12"]]}
// other imports...
import { FindConfig } from "@medusajs/medusa"
class MyCustomService extends TransactionBaseService {
async list(
selector: Selector<MyCustom>,
config?: FindConfig<MyCustom>
): Promise<MyCustom[]> {
const myCustomRepo =
this.activeManager_.getRepository(MyCustom)
const query = buildQuery(selector, config)
return await myCustomRepo.find(query)
}
}
```
You add a new parameter to the `list` method of type `FindConfig<MyCustom>`, then pass that parameter to `buildQuery`.
You can now pass pagination fields to the method in another resource:
```ts
const items = await myCustomService.list(
{},
{
take: 20,
}
)
```
This returns only the first `20` records in the database.
Other pagination fields include:
- `skip`: A number indicating how many items to skip before retrieving the records.
- `order`: An object whose keys are names of fields to sort the list by, and value is either `ASC` for ascending sorting or `DESC` for descending sorting.
---
## Select Fields and Relations
The `FindConfig` type accepts two additional properties:
- `select`: An array of strings, each being the name of a field in the data model that should be retrieved. When not specified, all fields are retrieved.
- `relations`: An array of strings, each being the name of a relation in the data model that should be retrieved.
For example:
```ts
const items = await myCustomService.list(
{},
{
select: ["name"],
}
)
```
---
## Management Operations
The repository provides all necessary operations to manage a data model including creating, updating, and deleting records. For a full list of repository methods, check out [Typeorm’s documentation](https://typeorm.io/repository-api).
---
## Example: CRUD Operations
Below is an example of a service that implements Create, Read, Update, and Delete (CRUD) operations on a data model.
<Details summaryContent="Example">
```ts title="src/services/my-custom.ts"
import {
FindConfig,
Selector,
TransactionBaseService,
buildQuery,
} from "@medusajs/medusa"
import { MedusaError } from "@medusajs/utils"
import { MyCustom } from "../models/my-custom"
class MyCustomService extends TransactionBaseService {
async list(
selector: Selector<MyCustom>,
config?: FindConfig<MyCustom>
): Promise<MyCustom[]> {
const myCustomRepo =
this.activeManager_.getRepository(MyCustom)
const query = buildQuery(selector, config)
return await myCustomRepo.find(query)
}
async retrieve(
id: string,
config?: FindConfig<MyCustom>
): Promise<MyCustom> {
const myCustomRepo =
this.activeManager_.getRepository(MyCustom)
const query = buildQuery(
{
id,
},
config
)
const custom = await myCustomRepo.findOne(query)
if (!custom) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Record was not found`
)
}
return custom
}
async create(data: Omit<MyCustom, "id">): Promise<MyCustom> {
return await this.atomicPhase_(async (manager) => {
const myCustomRepo = manager.getRepository(MyCustom)
const custom = myCustomRepo.create(data)
return await myCustomRepo.save(custom)
})
}
async update(
id: string,
data: Omit<MyCustom, "id">
): Promise<MyCustom> {
return await this.atomicPhase_(async (manager) => {
const myCustomRepo = manager.getRepository(MyCustom)
const custom = await this.retrieve(id)
for (const [key, value] of Object.entries(data)) {
custom[key] = value
}
return await myCustomRepo.save(custom)
})
}
async delete(id: string): Promise<void> {
return await this.atomicPhase_(async (manager) => {
const myCustomRepo = manager.getRepository(MyCustom)
await myCustomRepo.delete({
id,
})
})
}
}
export default MyCustomService
```
</Details>
@@ -0,0 +1,111 @@
export const metadata = {
title: `${pageNumber} Data Model Management`,
}
# {metadata.title}
In this chapter, you’ll learn how to manage a data model through a service.
## Entity Manager
An entity manager allows you to retrieve the repository of a data model, which you can then use to manage records of that model, such as create or update them.
The `TransactionBaseService` defines an `activeManager_` property, which is an instance of the entity manager. Services can use that property to retrieve a repository.
For example:
```ts title="src/services/my-custom.ts" highlights={[["7"]]}
import { TransactionBaseService } from "@medusajs/medusa"
import { MyCustom } from "../models/my-custom"
class MyCustomService extends TransactionBaseService {
async retrieve(id: string): Promise<MyCustom> {
const myCustomRepo =
this.activeManager_.getRepository(MyCustom)
// use repository...
}
}
export default MyCustomService
```
You use the entity manager’s `getRepository` method to retrieve the repository of a data model. The method accepts the data model as a parameter.
---
## Transaction Entity Managers
The function parameter of the `atomicPhase_` method accepts an instance of the transaction’s entity manager as a parameter. When retrieving a data model’s repository or performing database operations, you must use the transaction entity manager parameter.
### Retrieve Repository
```ts title="src/services/my-custom.ts" highlights={[["5"], ["6"]]}
class MyCustomService extends TransactionBaseService {
// ...
async create(data: any): Promise<MyCustom> {
return await this.atomicPhase_(async (manager) => {
const myCustomRepo = manager.getRepository(MyCustom)
// use repository...
})
}
}
```
### Custom Services
If you’re using methods of custom services within the transaction, you must call the `withTransaction` method of the service first and pass the transaction entity manager as a parameter. Then, chain the call with the call to the desired method.
For example:
```ts title="src/services/my-custom.ts" highlights={[["8"], ["9"]]}
class MyCustomService extends TransactionBaseService {
// ...
async create(data: any): Promise<MyCustom> {
return await this.atomicPhase_(async (manager) => {
// ...
const customData = await this.myOtherCustomService
.withTransaction(manager)
.retrieve(id)
})
}
}
```
### Commerce Module Services
All methods of Commerce Module services accept an object as a last parameter that's used to pass shared resources, such as the transaction manager in this case.
So, when using a Commerce Module service's method in a transaction, pass the transaction manager as part of the last object parameter.
For example:
```ts title="src/services/hello-world.ts" highlights={[["17"]]}
class HelloWorldService extends TransactionBaseService {
// ...
async updateProduct(
productId: string,
data: UpdateProductDTO
): Promise<ProductDTO> {
return await this.atomicPhase_(async (manager) => {
// example of a database operation
const product = await this.productModuleService.update(
[
{
...data,
id: productId,
},
],
{
transactionManager: manager,
}
)
return product[0]
})
}
}
```
@@ -0,0 +1,71 @@
export const metadata = {
title: `${pageNumber} Transactions in Services`,
}
# {metadata.title}
In this chapter, you’ll learn about transactions and how to use them in a service.
## What is a Transaction?
A transaction wraps a set of operations to ensure that when an error occurs, all database changes made by the executed operations are rolled back.
For example, if you have a service with a method that updates data of your custom data model, you can wrap the method’s operations in a transaction. Then, if an error occurs during the method's execution, the data update in the database is rolled back.
---
## How to Use Transactions?
To use transactions, change your service class to extend the `TransactionBaseService` imported from `@medusajs/medusa`:
```ts title="src/services/hello-world.ts" highlights={[["8"]]}
import { TransactionBaseService } from "@medusajs/medusa"
import { IProductModuleService } from "@medusajs/types"
type InjectedDependencies = {
productModuleService: IProductModuleService
}
class HelloWorldService extends TransactionBaseService {
protected productModuleService: IProductModuleService
constructor({ productModuleService }: InjectedDependencies) {
super(arguments[0])
this.productModuleService = productModuleService
}
// ...
}
export default HelloWorldService
```
Then, use the `TransactionBaseService`'s `atomicPhase_` method that allows you to wrap operations within transactions.
For example:
```ts title="src/services/hello-world.ts" highlights={[["11"]]}
// other imports...
import { ProductDTO, UpdateProductDTO } from "@medusajs/types"
class HelloWorldService extends TransactionBaseService {
// ...
async updateProduct(
productId: string,
data: UpdateProductDTO
): Promise<ProductDTO> {
return await this.atomicPhase_(async (manager) => {
// TODO perform db operation...
})
}
}
```
In the `updateProduct` method, you use the `atomicPhase_` method to wrap the method’s implementation.
The `atomicPhase_` method accepts a function as a parameter. All database operations performed in the function are wrapped in a transaction. So, if an error occurs in the function, the operations’ database changes are rolled back.
The data returned by the function parameter is then returned by the `atomicPhase_` method.
@@ -0,0 +1,253 @@
export const metadata = {
title: `${pageNumber} Advanced Workflow Example`,
}
# {metadata.title}
In this chapter, you’ll create an advanced workflow.
Workflows are most useful when performing tasks across services and integrations:
- You simplify complex flows or processes by splitting them into a series of steps.
- You avoid data inconsistency and loss through retry mechanisms and rollbacks.
- You execute the workflow from anywhere in your Medusa application.
The workflow you’ll build is an update-product workflow. You'll use an example `ErpService` that has methods to manage the product in a third-party ERP system, and the `ProductService`.
---
## 1. Create Workflow File
Start by creating the file `src/workflows/update-product-erp/index.ts` that will hold the constructed workflow.
In the file, add the type of the expected workflow input:
```ts title="src/workflows/update-product-erp/index.ts"
import { UpdateProductDTO } from "@medusajs/types"
export type UpdateProductAndErpWorkflowInput = UpdateProductDTO
```
The expected input is the data to update in the product along with the product’s ID.
---
## 2. Create Update Product Step
The first step in the workflow receives the product’s ID and the data to update, then updates the product.
Create the file `src/workflows/update-product-erp/steps/update-product.ts` with the following content:
export const updateProductHighlights = [
["13", "resolve", "Resolve the `ProductService` from the Medusa container."],
["16", "previousProductData", "Retrieve the `previousProductData` to pass it to the compensation function."],
["19", "", "Update the product."],
["33", "", "Revert the product’s data using the `previousProductData` passed from the step to the compensation function."]
]
```ts title="src/workflows/update-product-erp/steps/update-product.ts" highlights={updateProductHighlights}
import {
createStep,
StepResponse,
} from "@medusajs/workflows-sdk"
import { IProductModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { UpdateProductAndErpWorkflowInput } from ".."
const updateProduct = createStep(
"update-product",
async (input: UpdateProductAndErpWorkflowInput, context) => {
const productModuleService: IProductModuleService =
context.container.resolve(ModuleRegistrationName.PRODUCT)
const { id } = input
const previousProductData =
await productModuleService.retrieve(id)
const product = await productModuleService.update([input])
return new StepResponse(product, {
// pass to compensation function
previousProductData,
})
},
// compensation function
async ({ previousProductData }, context) => {
const productModuleService: IProductModuleService =
context.container.resolve(ModuleRegistrationName.PRODUCT)
const { type, ...previousData } = previousProductData
await productModuleService.update([
{
...previousData,
},
])
await productModuleService.updateTypes(type)
}
)
export default updateProduct
```
In the step:
- You resolve the `ProductService` from the Medusa container.
- You retrieve the `previousProductData` to pass it to the compensation function.
- You update and return the product.
You also pass a compensation function as a second parameter to `createStep`. The compensation function runs if an error occurs during the workflow execution.
In the compensation function, you revert the product’s data using the `previousProductData` passed from the step to the compensation function.
<Note title="Tip">
The compensation functions of each step ensure data consistency across Medusa and external services and systems when errors occur.
</Note>
---
## 3. Create Step 2: Update ERP
The second step in the workflow receives the same input. It updates the product’s details in the ERP system using the `ErpService`.
Create the file `src/workflows/update-product-erp/steps/update-erp.ts` with the following content:
export const updateErpHighlights = [
["12", "resolve", "Resolve the `ErpService` from the Medusa container."],
["17", "previousErpData", "Retrieve the `previousErpData` to pass it to the compensation function."],
["21", "updateProductErpData", "Update the product’s ERP data and return the data from the ERP system."],
["34", "updateProductErpData", "Revert the product's data in the ERP system to its previous state using the `previousErpData`."]
]
```ts title="src/workflows/update-product-erp/steps/update-erp.ts" highlights={updateErpHighlights}
import {
createStep,
StepResponse,
} from "@medusajs/workflows-sdk"
import { UpdateProductAndErpWorkflowInput } from ".."
import ErpService from "../../../services/erp"
const updateErp = createStep(
"update-erp",
async (input: UpdateProductAndErpWorkflowInput, context) => {
const erpService: ErpService =
context.container.resolve("erpService")
const { id, ...updatedData } = input
// get previous ERP data
const previousErpData =
await erpService.retrieveProductErpDetails(id)
const updatedErpData =
await erpService.updateProductErpData(id, updatedData)
return new StepResponse(updatedErpData, {
// pass to compensation function
previousErpData,
productId: id,
})
},
// compensation function
async ({ previousErpData, productId }, context) => {
const erpService: ErpService =
context.container.resolve("erpService")
await erpService.updateProductErpData(
productId,
previousErpData
)
}
)
export default updateErp
```
In the step:
- You resolve the `ErpService` from the Medusa container.
- You retrieve the `previousErpData` to pass it to the compensation function.
- You update the product’s ERP data and return the data from the ERP system.
You also pass a compensation function as a second parameter to `createStep`. In the compensation function, you revert the product's data in the ERP system to its previous state.
---
## 4. Create Workflow
With the steps ready, you'll create the workflow that runs these steps to update the product’s data both in Medusa and the external ERP system.
Change the content of `src/workflows/update-product-erp/index.ts` to the following:
```ts title="src/workflows/update-product-erp/index.ts"
import { createWorkflow } from "@medusajs/workflows-sdk"
import { UpdateProductDTO, ProductDTO } from "@medusajs/types"
import updateProduct from "./steps/update-product"
import updateErp from "./steps/update-erp"
export type UpdateProductAndErpWorkflowInput = UpdateProductDTO
type WorkflowOutput = {
product: ProductDTO
erpData: Record<string, unknown>
}
const updateProductAndErpWorkflow = createWorkflow<
UpdateProductAndErpWorkflowInput,
WorkflowOutput
>("update-product-and-erp", function (input) {
const product = updateProduct(input)
const erpData = updateErp(input)
return {
product,
erpData,
}
})
export default updateProductAndErpWorkflow
```
In the workflow construction function, you first run the `updateProduct` step, then the `updateErp` step. You return as the workflow’s result an object holding the updated product and ERP data.
---
## 5. Execute Workflow
You can now use and execute your workflow in your Medusa application.
To execute the workflow in an API route, create the file `src/api/store/products/[id]/erp/route.ts` with the following content:
```ts title="src/api/store/products/[id]/erp/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
import updateProductAndErpWorkflow, {
UpdateProductAndErpWorkflowInput,
} from "../../../../../workflows/update-product-erp"
export const POST = async (
req: MedusaRequest,
res: MedusaResponse
) => {
// skipping validation for simplicity
const productData: UpdateProductAndErpWorkflowInput = {
id: req.params.id,
...req.body,
}
const { result } = await updateProductAndErpWorkflow(
req.scope
).run({
input: productData,
})
res.json(result)
}
```
In this `POST` API route, you retrieve the product’s ID from the path parameter and the data to update from the request body. You then execute the workflow by passing it the retrieved data as an input.
The route returns the result of the workflow, which is an object holding both the update product’s details and the ERP details.
@@ -0,0 +1,111 @@
export const metadata = {
title: `${pageNumber} Compensation Function`,
}
# {metadata.title}
In this chapter, you'll learn how to add a compensation function to a step.
## Compensation Function
Errors can occur in a workflow. To avoid data inconsistency, you can pass a compensation function as a third parameter to the `createStep` function.
The compensation function only runs if an error occurs throughout the Workflow. It’s useful to undo or roll back actions you’ve performed in a step.
For example, change step one to add a compensation function and step two to throw an error:
```ts title="src/workflows/hello-world.ts" highlights={[["10"], ["11"], ["12"]]}
const step1 = createStep(
"step-1",
async () => {
const message = `Hello from step one!`
console.log(message)
return new StepResponse(message)
},
async () => {
console.log("Oops! Rolling back my changes...")
}
)
const step2 = createStep(
"step-2",
async ({ name }: WorkflowInput) => {
throw new Error("Throwing an error...")
}
)
```
### Test Compensation Function
1. Use the steps in a workflow. For example:
```ts title="src/workflows/hello-world.ts"
import {
// other imports...
createWorkflow,
} from "@medusajs/workflows-sdk"
// ...
type WorkflowOutput = {
message: string
}
const myWorkflow = createWorkflow<
WorkflowInput,
WorkflowOutput
>("hello-world", function (input) {
const str1 = step1()
// to pass input
const str2 = step2(input)
return {
message: str1,
}
})
export default myWorkflow
```
2. Execute the workflow from a resource, such as an API route:
```ts title="src/api/store/workflow/route.ts"
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import myWorkflow from "../../../workflows/hello-world"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
) {
const { result } = await myWorkflow(req.scope)
.run({
input: {
name: req.query.name as string,
},
})
res.send(result)
}
```
3. Run the Medusa application:
```bash npm2yarn
npm run dev
```
4. Send a `GET` request to `/store/workflow`:
```bash apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/workflows"
curl http://localhost:9000/store/workflow
```
In the console, you'll see:
- `Hello from step one!` logged in the terminal, indicating that the first step ran successfully.
- `Oops! Rolling back my changes...` logged in the terminal, indicating that the second step failed and the compensation function of the first step ran consequently.
@@ -0,0 +1,96 @@
export const metadata = {
title: `${pageNumber} Constraints on Workflow Constructor Function`,
}
# {metadata.title}
This chapter lists some constraints to keep in mind when defining Workflow constructor functions.
## No Arrow Functions
The function passed to the `createWorkflow` function can’t be an arrow function:
```ts highlights={[["5", "=>", "Function can't be an arrow function"], ["13", "", "Correct way of defining the function."]]}
// Don't
const myWorkflow = createWorkflow<
WorkflowInput,
WorkflowOutput
>("hello-world", (input) => {
// ...
})
// Do
const myWorkflow = createWorkflow<
WorkflowInput,
WorkflowOutput
>("hello-world", function (input) {
// ...
})
```
## No Async Functions
The function passed to the `createWorkflow` can’t be an async function:
```ts highlights={[["5", "async", "Function can't be async."], ["13", "", "Correct way of defining the function."]]}
// Don't
const myWorkflow = createWorkflow<
WorkflowInput,
WorkflowOutput
>("hello-world", async function (input) {
// ...
})
// Do
const myWorkflow = createWorkflow<
WorkflowInput,
WorkflowOutput
>("hello-world", function (input) {
// ...
})
```
## No Direct Data Manipulation
Since the constructor function only defines how the workflow works, you can’t directly manipulate data within the function. Instead, use the `transform` function:
export const highlights = [
["10", "", "Don't manipulate data directly."],
["22", "transform", "Use the `transform` function to manipulate data."]
]
```ts highlights={highlights}
// Don't
const myWorkflow = createWorkflow<
WorkflowInput,
WorkflowOutput
>("hello-world", function (input) {
const str1 = step1(input)
const str2 = step2(input)
return {
message: `${input.str1}${input.str2}`,
}
})
// Do
const myWorkflow = createWorkflow<
WorkflowInput,
WorkflowOutput
>("hello-world", function (input) {
const str1 = step1(input)
const str2 = step2(input)
const result = transform(
{
str1,
str2,
},
(input) => ({
message: `${input.str1}${input.str2}`,
})
)
return result
})
```
@@ -0,0 +1,58 @@
export const metadata = {
title: `${pageNumber} Running Workflow Steps in Parallel`,
}
# {metadata.title}
In this chapter, you’ll learn how to run workflow steps in parallel.
## parallelize Utility Function
If your workflow has steps that don’t rely on one another’s results, you can run them in parallel. The workflow will wait until all specified steps are finished before continuing with the rest of its implementation.
The `parallelize` utility function imported from the `@medusajs/workflows-sdk` package allows you to run the steps in parallel.
For example:
export const highlights = [
["23", "[prices, productSalesChannel]", "The result of the steps. `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`."],
["23", "parallelize", "Run the steps passed as parameters in parallel."],
]
```ts highlights={highlights}
import {
createWorkflow,
parallelize,
} from "@medusajs/workflows-sdk"
import { ProductDTO } from "@medusajs/types"
import {
createProductStep,
getProductStep,
createPricesStep,
attachProductToSalesChannelStep,
} from "./steps"
interface WorkflowInput {
title: string
}
const myWorkflow = createWorkflow<
WorkflowInput,
ProductDTO
>("my-workflow", (input) => {
const product = createProductStep(input)
const [prices, productSalesChannel] = parallelize(
createPricesStep(product),
attachProductToSalesChannelStep(product)
)
const id = product.id
return getProductStep(product.id)
}
)
```
The `parallelize` function accepts the steps to run in parallel as a parameter.
It returns an array of each step’s result. The results are ordered in the result array by the order they're passed in the function's parameter.