docs: add routing page (#9550)
- Add a new homepage to `book` project for the routing page - Move all main doc pages to be under `/v2/learn` (and added redirects + fixed links across docs) - Other: add admin components to resources dropdown + fixes to search on mobile. Closes DX-955 Preview: https://docs-v2-git-docs-router-page-medusajs.vercel.app/v2
This commit is contained in:
1 parent
7a47f5211d
commit
0a37675f0e
223 files changed
+2549
-696
No files matched your search
@@ -0,0 +1,58 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Admin Development Constraints`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
This chapter lists some constraints of admin widgets and UI routes.
|
||||
|
||||
## Arrow Functions
|
||||
|
||||
Widget and UI route components must be created as arrow functions.
|
||||
|
||||
export const arrowHighlights = [
|
||||
["2", "function", "Don't declare the widget / UI route as a function."],
|
||||
["7", "() => ", "Use arrow functions when creating a widget / UI route."]
|
||||
]
|
||||
|
||||
```ts highlights={arrowHighlights}
|
||||
// Don't
|
||||
function ProductWidget() {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Do
|
||||
const ProductWidget = () => {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Widget Zone
|
||||
|
||||
A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable.
|
||||
|
||||
export const zoneHighlights = [
|
||||
["3", "`product.details.before`", "Don't specify the value of `zone` as a template literal."],
|
||||
["9", "ZONE", "Don't specify a variable as the value of `zone`."],
|
||||
["14", `"product.details.before"`, "Wrap the value of `zone` in double or single quotes."]
|
||||
]
|
||||
|
||||
```ts highlights={zoneHighlights}
|
||||
// Don't
|
||||
export const config = defineWidgetConfig({
|
||||
zone: `product.details.before`,
|
||||
})
|
||||
|
||||
// Don't
|
||||
const ZONE = "product.details.after"
|
||||
export const config = defineWidgetConfig({
|
||||
zone: ZONE,
|
||||
})
|
||||
|
||||
// Do
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Admin Development`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
## Admin Components List
|
||||
|
||||
To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](!resources!/admin-component) to find common components.
|
||||
@@ -0,0 +1,101 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Admin Development Tips`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll find some tips for your admin development.
|
||||
|
||||
## Send Requests to API Routes
|
||||
|
||||
To send a request to an API route in the Medusa Application, use the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch).
|
||||
|
||||
For example:
|
||||
|
||||
export const fetchHighlights = [
|
||||
["14", "fetch", "Send a request to the `/admin/products` API route."]
|
||||
]
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx" highlights={fetchHighlights}
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { Container } from "@medusajs/ui"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
const ProductWidget = () => {
|
||||
const [productsCount, setProductsCount] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
return
|
||||
}
|
||||
|
||||
fetch(`/admin/products`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ count }) => {
|
||||
setProductsCount(count)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [loading])
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
{loading && <span>Loading...</span>}
|
||||
{!loading && <span>You have {productsCount} Product(s).</span>}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.list.before",
|
||||
})
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
In this example, you send a request to the [List Products API route](!api!/admin#products_getproducts) and show the count of products in a widget.
|
||||
|
||||
---
|
||||
|
||||
## Routing Functionalities
|
||||
|
||||
To navigate or link to other pages, or perform other routing functionalities, use the [react-router-dom](https://reactrouter.com/en/main) package. It's installed in your project through the Medusa Admin.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["9", "Link", "Add a link to another page"],
|
||||
["9", '"/orders', "Add the path without the `/app` prefix."]
|
||||
]
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx" highlights={highlights}
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { Container } from "@medusajs/ui"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
// The widget
|
||||
const ProductWidget = () => {
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<Link to={"/orders"}>View Orders</Link>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
// The widget's configurations
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
This adds a widget in a product's details page with a link to the Orders page. The link's path must be without the `/app` prefix.
|
||||
|
||||
<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,168 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Admin UI Routes`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
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 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"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
|
||||
const CustomPage = () => {
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">This is my custom route</Heading>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomPage
|
||||
```
|
||||
|
||||
The new page’s path is the file’s path relative to `src/admin/routes`. So, the above UI route is a new page added at the path `localhost:9000/app/custom`.
|
||||
|
||||
<Note title="Important" type="warning">
|
||||
|
||||
The UI route component must be created as an arrow function.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## 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:9000/app/custom` to see your custom page.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["16", "label", "The label of the UI route's sidebar item."],
|
||||
["17", "icon", "The icon of the UI route's sidebar item."]
|
||||
]
|
||||
|
||||
```tsx title="src/admin/routes/custom/page.tsx" highlights={highlights}
|
||||
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||
import { ChatBubbleLeftRight } from "@medusajs/icons"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
|
||||
const CustomPage = () => {
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">This is my custom route</Heading>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineRouteConfig({
|
||||
label: "Custom Route",
|
||||
icon: ChatBubbleLeftRight,
|
||||
})
|
||||
|
||||
export default CustomPage
|
||||
```
|
||||
|
||||
The configuration object is created using the `defineRouteConfig` function imported from `@medusajs/admin-sdk`. It accepts the following properties:
|
||||
|
||||
- `label`: the sidebar item’s label.
|
||||
- `icon`: an optional React component used as an icon in the sidebar.
|
||||
|
||||
The above example adds a new sidebar item with the label `Custom Route` and an icon from the [Medusa UI Icons package](!ui!/icons/overview).
|
||||
|
||||
---
|
||||
|
||||
## Create Settings Page
|
||||
|
||||
To create a page under the settings section of the admin dashboard, create the UI route file under the path `src/admin/routes/settings`.
|
||||
|
||||
For example:
|
||||
|
||||
```tsx title="src/admin/routes/settings/custom/page.tsx"
|
||||
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
|
||||
const CustomSettingPage = () => {
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h1">Custom Setting Page</Heading>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineRouteConfig({
|
||||
label: "Custom",
|
||||
})
|
||||
|
||||
export default CustomSettingPage
|
||||
```
|
||||
|
||||
This adds a page under the path `/app/settings/custom`. An item is also added to the settings sidebar with the label `Custom`.
|
||||
|
||||
---
|
||||
|
||||
## 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, 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."], ["10", "{id}", "Show the path parameter."]]}
|
||||
import { useParams } from "react-router-dom"
|
||||
import { Container } from "@medusajs/ui"
|
||||
|
||||
const CustomPage = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h1">Passed ID: {id}</Heading>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomPage
|
||||
```
|
||||
|
||||
You access the passed parameter using `react-router-dom`'s [useParams hook](https://reactrouter.com/en/main/hooks/use-params).
|
||||
|
||||
If you run the Medusa application and go to `localhost:9000/app/custom/123`, you'll see `123` printed in the page.
|
||||
|
||||
---
|
||||
|
||||
## Admin Components List
|
||||
|
||||
To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](!resources!/admin-component) to find common components.
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Table } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Admin Widgets`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
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. The file must also export the widget’s configurations.
|
||||
|
||||
For example, create the file `src/admin/widgets/product-widget.tsx` with the following content:
|
||||
|
||||
export const widgetHighlights = [
|
||||
["5", "ProductWidget", "The React component of the product widget."],
|
||||
["17", "zone", "The zone to inject the widget to."]
|
||||
]
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights}
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
|
||||
// The widget
|
||||
const ProductWidget = () => {
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">Product Widget</Heading>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
// The widget's configurations
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
The widget only shows the heading `Product Widget`.
|
||||
|
||||
Use the `defineWidgetConfig` function imported from `@medusajs/admin-sdk` to create and export the widget's configurations. It accepts as a parameter an object with the following property:
|
||||
|
||||
- `zone`: A string or an array of strings, each being the name of the zone to inject the widget into.
|
||||
|
||||
In the example above, the widget is injected at the top of a product’s details.
|
||||
|
||||
<Note title="Important" type="warning">
|
||||
|
||||
The widget component must be created as an arrow function.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## 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 top of the page.
|
||||
|
||||
---
|
||||
|
||||
## Detail Widget Props
|
||||
|
||||
Widgets that are injected into a details page (for example, `product.details.before`) receive a `data` prop, which is the main data of the details page (for example, the product object).
|
||||
|
||||
For example:
|
||||
|
||||
export const detailHighlights = [
|
||||
["10", "data", "Receive the data as a prop."],
|
||||
["11", "AdminProduct", "Pass the expected type of `data` as a type argument."],
|
||||
["16", "data.title", "Show the product's title."]
|
||||
]
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights}
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import {
|
||||
DetailWidgetProps,
|
||||
AdminProduct,
|
||||
} from "@medusajs/framework/types"
|
||||
|
||||
// The widget
|
||||
const ProductWidget = ({
|
||||
data,
|
||||
}: DetailWidgetProps<AdminProduct>) => {
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">
|
||||
Product Widget {data.title}
|
||||
</Heading>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
// The widget's configurations
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
Notice that the type of the props is `DetailWidgetProps`, which accepts as a type argument the expected type of `data`.
|
||||
|
||||
---
|
||||
|
||||
## Injection Zone
|
||||
|
||||
Refer to [this reference](!resources!/admin-widget-injection-zones) for the full list of injection zones and their props.
|
||||
@@ -0,0 +1,207 @@
|
||||
import { Details } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Pass Additional Data to Medusa's API Route`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to pass additional data in requests to Medusa's API Route.
|
||||
|
||||
## Why Pass Additional Data?
|
||||
|
||||
Some of Medusa's API Routes accept an `additional_data` parameter whose type is an object. The API Route passes the `additional_data` to the workflow, which in turn passes it to its hooks.
|
||||
|
||||
This is useful when you have a link from your custom module to a commerce module, and you want to perform an additional action when a request is sent to an existing API route.
|
||||
|
||||
For example, the [Create Product API Route](!api!/admin#products_postproducts) accepts an `additional_data` parameter. If you have a data model linked to it, you consume the `productsCreated` hook to create a record of the data model using the custom data and link it to the product.
|
||||
|
||||
### API Routes Accepting Additional Data
|
||||
|
||||
<Details summaryContent="API Routes List">
|
||||
|
||||
- Campaigns
|
||||
- [Create Campaign](!api!/admin#campaigns_postcampaigns)
|
||||
- [Update Campaign](!api!/admin#campaigns_postcampaignsid)
|
||||
- Cart
|
||||
- [Create Cart](!api!/store#carts_postcarts)
|
||||
- [Update Cart](!api!/store#carts_postcartsid)
|
||||
- Customers
|
||||
- [Create Customer](!api!/admin#customers_postcustomers)
|
||||
- [Update Customer](!api!/admin#customers_postcustomersid)
|
||||
- [Create Address](!api!/admin#customers_postcustomersidaddresses)
|
||||
- [Update Address](!api!/admin#customers_postcustomersidaddressesaddress_id)
|
||||
- Draft Orders
|
||||
- [Create Draft Order](!api!/admin#draft-orders_postdraftorders)
|
||||
- Orders
|
||||
- [Complete Orders](!api!/admin#orders_postordersidcomplete)
|
||||
- [Cancel Order's Fulfillment](!api!/admin#orders_postordersidfulfillmentsfulfillment_idcancel)
|
||||
- [Create Shipment](!api!/admin#orders_postordersidfulfillmentsfulfillment_idshipments)
|
||||
- [Create Fulfillment](!api!/admin#orders_postordersidfulfillments)
|
||||
- Products
|
||||
- [Create Product](!api!/admin#products_postproducts)
|
||||
- [Update Product](!api!/admin#products_postproductsid)
|
||||
- [Create Product Variant](!api!/admin#products_postproductsidvariants)
|
||||
- [Update Product Variant](!api!/admin#products_postproductsidvariantsvariant_id)
|
||||
- [Create Product Option](!api!/admin#products_postproductsidoptions)
|
||||
- [Update Product Option](!api!/admin#products_postproductsidoptionsoption_id)
|
||||
- Promotions
|
||||
- [Create Promotion](!api!/admin#promotions_postpromotions)
|
||||
- [Update Promotion](!api!/admin#promotions_postpromotionsid)
|
||||
|
||||
</Details>
|
||||
|
||||
---
|
||||
|
||||
## How to Pass Additional Data
|
||||
|
||||
### 1. Specify Validation of Additional Data
|
||||
|
||||
Before passing custom data in the `additional_data` object parameter, you must specify validation rules for the allowed properties in the object.
|
||||
|
||||
To do that, use the middleware route object defined in `src/api/middlewares.ts`.
|
||||
|
||||
For example, create the file `src/api/middlewares.ts` with the following content:
|
||||
|
||||
```ts title="src/api/middlewares.ts"
|
||||
import { defineMiddlewares } from "@medusajs/medusa"
|
||||
import { z } from "zod"
|
||||
|
||||
export default defineMiddlewares({
|
||||
routes: [
|
||||
{
|
||||
method: "POST",
|
||||
matcher: "/admin/products",
|
||||
additionalDataValidator: {
|
||||
brand: z.string().optional(),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
The middleware route object accepts an optional parameter `additionalDataValidator` whose value is an object of key-value pairs. The keys indicate the name of accepted properties in the `additional_data` parameter, and the value is [Zod](https://zod.dev/) validation rules of the property.
|
||||
|
||||
In this example, you indicate that the `additional_data` parameter accepts a `brand` property whose value is an optional string.
|
||||
|
||||
<Note>
|
||||
|
||||
Refer to [Zod's documentation](https://zod.dev) for all available validation rules.
|
||||
|
||||
</Note>
|
||||
|
||||
### 2. Pass the Additional Data in a Request
|
||||
|
||||
You can now pass a `brand` property in the `additional_data` parameter of a request to the Create Product API Route.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/admin/products' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer {token}' \
|
||||
--data '{
|
||||
"title": "Product 1",
|
||||
"options": [
|
||||
{
|
||||
"title": "Default option",
|
||||
"values": ["Default option value"]
|
||||
}
|
||||
],
|
||||
"additional_data": {
|
||||
"brand": "Acme"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Make sure to replace the `{token}` in the authorization header with an admin user's authentication token.
|
||||
|
||||
</Note>
|
||||
|
||||
In this request, you pass in the `additional_data` parameter a `brand` property and set its value to `Acme`.
|
||||
|
||||
The `additional_data` is then passed to hooks in the `createProductsWorkflow` used by the API route.
|
||||
|
||||
---
|
||||
|
||||
## Use Additional Data in a Hook
|
||||
|
||||
<Note>
|
||||
|
||||
Learn about workflow hooks in [this guide](../../workflows/workflow-hooks/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
Step functions consuming the workflow hook can access the `additional_data` in the first parameter.
|
||||
|
||||
For example, consider you want to store the data passed in `additional_data` in the product's `metadata` property.
|
||||
|
||||
To do that, create the file `src/workflows/hooks/product-created.ts` with the following content:
|
||||
|
||||
```ts title="src/workflows/hooks/product-created.ts"
|
||||
import { StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
createProductsWorkflow.hooks.productsCreated(
|
||||
async ({ products, additional_data }, { container }) => {
|
||||
if (!additional_data.brand) {
|
||||
return
|
||||
}
|
||||
|
||||
const productModuleService = container.resolve(
|
||||
Modules.PRODUCT
|
||||
)
|
||||
|
||||
await productModuleService.upsertProducts(
|
||||
products.map((product) => ({
|
||||
...product,
|
||||
metadata: {
|
||||
...product.metadata,
|
||||
brand: additional_data.brand,
|
||||
},
|
||||
}))
|
||||
)
|
||||
|
||||
return new StepResponse(products, {
|
||||
products,
|
||||
additional_data,
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This consumes the `productsCreated` hook, which runs after the products are created.
|
||||
|
||||
If `brand` is passed in `additional_data`, you resolve the Product Module's main service and use its `upsertProducts` method to update the products, adding the brand to the `metadata` property.
|
||||
|
||||
### Compensation Function
|
||||
|
||||
Hooks also accept a compensation function as a second parameter to undo the actions made by the step function.
|
||||
|
||||
For example, pass the following second parameter to the `productsCreated` hook:
|
||||
|
||||
```ts title="src/workflows/hooks/product-created.ts"
|
||||
createProductsWorkflow.hooks.productsCreated(
|
||||
async ({ products, additional_data }, { container }) => {
|
||||
// ...
|
||||
},
|
||||
async ({ products, additional_data }, { container }) => {
|
||||
if (!additional_data.brand) {
|
||||
return
|
||||
}
|
||||
|
||||
const productModuleService = container.resolve(
|
||||
Modules.PRODUCT
|
||||
)
|
||||
|
||||
await productModuleService.upsertProducts(
|
||||
products
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This updates the product to their original state before adding the brand to their `metadata` property.
|
||||
@@ -0,0 +1,120 @@
|
||||
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
|
||||
|
||||
The `storeCors` and `adminCors` properties of Medusa's `http` configuration set the allowed origins for routes starting with `/store` and `/admin` respectively.
|
||||
|
||||
These configurations accept a URL pattern to identify allowed origins.
|
||||
|
||||
For example:
|
||||
|
||||
```js title="medusa-config.ts"
|
||||
module.exports = defineConfig({
|
||||
projectConfig: {
|
||||
http: {
|
||||
storeCors: "http://localhost:8000",
|
||||
adminCors: "http://localhost:7001",
|
||||
// ...
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
This allows the `http://localhost:7001` origin to access the Admin API Routes, and the `http://localhost:8000` origin to access Store API Routes.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Learn more about the CORS configurations in [this resource guide](!resources!/references/medusa-config#http).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## CORS in Store and Admin Routes
|
||||
|
||||
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/framework/http"
|
||||
|
||||
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 lead to a CORS error.
|
||||
|
||||
You can do that in the exported middlewares configurations in `src/api/middlewares.ts`.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [["25", "parseCorsOrigins", "A utility function that parses the CORS configurations in `medusa-config.ts`"]]
|
||||
|
||||
```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
|
||||
import { defineMiddlewares } from "@medusajs/medusa"
|
||||
import type {
|
||||
MedusaNextFunction,
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import { ConfigModule } from "@medusajs/framework/types"
|
||||
import { parseCorsOrigins } from "@medusajs/framework/utils"
|
||||
import cors from "cors"
|
||||
|
||||
export default defineMiddlewares({
|
||||
routes: [
|
||||
{
|
||||
matcher: "/custom*",
|
||||
middlewares: [
|
||||
(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse,
|
||||
next: MedusaNextFunction
|
||||
) => {
|
||||
const configModule: ConfigModule =
|
||||
req.scope.resolve("configModule")
|
||||
|
||||
return cors({
|
||||
origin: parseCorsOrigins(
|
||||
configModule.projectConfig.http.storeCors
|
||||
),
|
||||
credentials: true,
|
||||
})(req, res, next)
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`.
|
||||
@@ -0,0 +1,284 @@
|
||||
import { Table } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Throwing and Handling Errors`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this guide, you'll learn how to throw errors in your Medusa application, how it affects an API route's response, and how to change the default error handler of your Medusa application.
|
||||
|
||||
## Throw MedusaError
|
||||
|
||||
When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError`, which is imported from `@medusajs/framework/utils`.
|
||||
|
||||
The Medusa application's API route error handler then wraps your thrown error in a uniform object and returns it in the response.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import { MedusaError } from "@medusajs/framework/utils"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
if (!req.query.q) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"The `q` query parameter is required."
|
||||
)
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The `MedusaError` class accepts in its constructor two parameters:
|
||||
|
||||
1. The first is the error's type. `MedusaError` has a static property `Types` that you can use. `Types` is an enum whose possible values are explained in the next section.
|
||||
2. The second is the message to show in the error response.
|
||||
|
||||
### Error Object in Response
|
||||
|
||||
The error object returned in the response has two properties:
|
||||
|
||||
- `type`: The error's type.
|
||||
- `message`: The error message, if available.
|
||||
- `code`: A common snake-case code. Its values can be:
|
||||
- `invalid_request_error` for the `DUPLICATE_ERROR` type.
|
||||
- `api_error`: for the `DB_ERROR` type.
|
||||
- `invalid_state_error` for `CONFLICT` error type.
|
||||
- `unknown_error` for any unidentified error type.
|
||||
- For other error types, this property won't be available unless you provide a code as a third parameter to the `MedusaError` constructor.
|
||||
|
||||
### MedusaError Types
|
||||
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Type</Table.HeaderCell>
|
||||
<Table.HeaderCell>Description</Table.HeaderCell>
|
||||
<Table.HeaderCell className="w-1/5">Status Code</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`DB_ERROR`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates a database error.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`500`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`DUPLICATE_ERROR`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates a duplicate of a record already exists. For example, when trying to create a customer whose email is registered by another customer.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`422`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`INVALID_ARGUMENT` and `UNEXPECTED_STATE`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates an error that occurred due to incorrect arguments or other unexpected state.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`500`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`INVALID_DATA`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates a validation error.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`400`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`UNAUTHORIZED`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates that a user is not authorized to perform an action or access a route.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`401`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`NOT_FOUND`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates that the requested resource, such as a route or a record, isn't found.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`404`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`NOT_ALLOWED`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates that an operation isn't allowed.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`400`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`CONFLICT`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates that a request conflicts with another previous or ongoing request. The error message in this case is ignored for a default message.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`409`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
`PAYMENT_AUTHORIZATION_ERROR`
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Indicates an error has occurred while authorizing a payment.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`422`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
|
||||
Other error types
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
Any other error type results in an `unknown_error` code and message.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
`500`
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
|
||||
---
|
||||
|
||||
## Override Error Handler
|
||||
|
||||
The `defineMiddlewares` function used to apply middlewares on routes accepts an `errorHandler` in its object parameter. Use it to override the default error handler for API routes.
|
||||
|
||||
<Note>
|
||||
|
||||
This error handler will also be used for errors thrown in Medusa's API routes and resources.
|
||||
|
||||
</Note>
|
||||
|
||||
For example, create `src/api/middlewares.ts` with the following:
|
||||
|
||||
```ts title="src/api/middlewares.ts" collapsibleLines="1-8" expandMoreLabel="Show Imports"
|
||||
import {
|
||||
defineMiddlewares,
|
||||
MedusaNextFunction,
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import { MedusaError } from "@medusajs/framework/utils"
|
||||
|
||||
export default defineMiddlewares({
|
||||
errorHandler: (
|
||||
error: MedusaError | any,
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse,
|
||||
next: MedusaNextFunction
|
||||
) => {
|
||||
res.status(400).json({
|
||||
error: "Something happened.",
|
||||
})
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The `errorHandler` property's value is a function that accepts four parameters:
|
||||
|
||||
1. The error thrown. Its type can be `MedusaError` or any other thrown error type.
|
||||
2. A request object of type `MedusaRequest`.
|
||||
3. A response object of type `MedusaResponse`.
|
||||
4. A function of type MedusaNextFunction that executes the next middleware in the stack.
|
||||
|
||||
This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message.
|
||||
@@ -0,0 +1,45 @@
|
||||
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.
|
||||
|
||||
## HTTP Method Handler
|
||||
|
||||
An API route is created for every HTTP method you export a handler function for in a route file.
|
||||
|
||||
Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`.
|
||||
|
||||
For example, create the file `src/api/hello-world/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/hello-world/route.ts"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: "[GET] Hello world!",
|
||||
})
|
||||
}
|
||||
|
||||
export const POST = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: "[POST] Hello world!",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This adds two API Routes:
|
||||
|
||||
- A `GET` route at `http://localhost:9000/hello-world`.
|
||||
- A `POST` route at `http://localhost:9000/hello-world`.
|
||||
@@ -0,0 +1,217 @@
|
||||
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. It's executed before the route handler function.
|
||||
|
||||
Middlwares are used to guard API routes, parse request content types other than `application/json`, manipulate request data, and more.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
As Medusa's server is based on Express, you can use any [Express middleware](https://expressjs.com/en/resources/middleware.html).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Middleware?
|
||||
|
||||
Middlewares are defined in the special file `src/api/middlewares.ts`. Use the `defineMiddlewares` function imported from `@medusajs/medusa` to define the middlewares, and export its value.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/middlewares.ts"
|
||||
import { defineMiddlewares } from "@medusajs/medusa"
|
||||
import type {
|
||||
MedusaNextFunction,
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
|
||||
export default defineMiddlewares({
|
||||
routes: [
|
||||
{
|
||||
matcher: "/custom*",
|
||||
middlewares: [
|
||||
(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse,
|
||||
next: MedusaNextFunction
|
||||
) => {
|
||||
console.log("Received a request!")
|
||||
|
||||
next()
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
The `defineMiddlewares` function accepts a middleware configurations object that has the property `routes`. `routes`'s value is an array of middleware route objects, each having the following properties:
|
||||
|
||||
- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. The regular expression must be compatible with [path-to-regexp](https://github.com/pillarjs/path-to-regexp).
|
||||
- `middlewares`: An array of middleware functions.
|
||||
|
||||
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 `/custom`.
|
||||
|
||||
---
|
||||
|
||||
## Test the Middleware
|
||||
|
||||
To test the middleware:
|
||||
|
||||
1. Start the application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
2. Send a request to any API route starting with `/custom`.
|
||||
3. See the following message in the terminal:
|
||||
|
||||
```bash
|
||||
Received a request!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use Middlewares
|
||||
|
||||
<Note type="success" title="Use middlewares when">
|
||||
|
||||
- You want to protect API routes by a custom condition.
|
||||
- You're modifying the request body.
|
||||
|
||||
</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>
|
||||
|
||||
---
|
||||
|
||||
## Middleware for Routes with Path Parameters
|
||||
|
||||
To indicate a path parameter in a middleware's `matcher` pattern, use the format `:{param-name}`.
|
||||
|
||||
For example:
|
||||
|
||||
export const pathParamHighlights = [["11", ":id", "Indicates that the API route accepts an `id` path parameter."]]
|
||||
|
||||
```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports" highlights={pathParamHighlights}
|
||||
import { defineMiddlewares } from "@medusajs/medusa"
|
||||
import type {
|
||||
MedusaNextFunction,
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
|
||||
export default defineMiddlewares({
|
||||
routes: [
|
||||
{
|
||||
matcher: "/custom/:id",
|
||||
middlewares: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
This applies a middleware to the routes defined in the file `src/api/custom/[id]/route.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Restrict HTTP Methods
|
||||
|
||||
Restrict which HTTP methods the middleware is applied to using the `method` property of the middleware route object.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [["12", "method", "Apply the middleware only on `POST` requests"]]
|
||||
|
||||
```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import { defineMiddlewares } from "@medusajs/medusa"
|
||||
import type {
|
||||
MedusaNextFunction,
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
|
||||
export default defineMiddlewares({
|
||||
routes: [
|
||||
{
|
||||
matcher: "/custom*",
|
||||
method: ["POST", "PUT"],
|
||||
middlewares: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
`method`'s value is one or more HTTP methods to apply the middleware to.
|
||||
|
||||
This example applies the middleware only when a `POST` or `PUT` request is sent to an API route path starting with `/custom`.
|
||||
|
||||
---
|
||||
|
||||
## Request URLs with Trailing Backslashes
|
||||
|
||||
A middleware whose `matcher` pattern doesn't end with a backslash won't be applied for requests to URLs with a trailing backslash.
|
||||
|
||||
For example, consider you have the following middleware:
|
||||
|
||||
```ts collapsibleLines="1-7" expandMoreLabel="Show Imports"
|
||||
import { defineMiddlewares } from "@medusajs/medusa"
|
||||
import type {
|
||||
MedusaNextFunction,
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
|
||||
export default defineMiddlewares({
|
||||
routes: [
|
||||
{
|
||||
matcher: "/custom",
|
||||
middlewares: [
|
||||
(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse,
|
||||
next: MedusaNextFunction
|
||||
) => {
|
||||
console.log("Received a request!")
|
||||
|
||||
next()
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
If you send a request to `http://localhost:9000/custom`, the middleware will run.
|
||||
|
||||
However, if you send a request to `http://localhost:9000/custom/`, the middleware won't run.
|
||||
|
||||
In general, avoid adding trailing backslashes when sending requests to API routes.
|
||||
@@ -0,0 +1,14 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} API Routes Advanced Guides`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you'll focus more on API routes to learn about topics such as:
|
||||
|
||||
- Creating API routes for different HTTP methods.
|
||||
- Accepting parameters in your API routes.
|
||||
- Formatting response data and headers.
|
||||
- Applying middlewares on API routes.
|
||||
- Validating request body parameters.
|
||||
- Protecting API routes by requiring user authentication.
|
||||
@@ -0,0 +1,155 @@
|
||||
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 create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`.
|
||||
|
||||
For example, to create an API Route at the path `/hello-world/:id`, where `:id` is a path parameter, create the file `src/api/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/hello-world/[id]/route.ts" highlights={singlePathHighlights}
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: `[GET] Hello ${req.params.id}!`,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs.
|
||||
|
||||
### Multiple Path Parameters
|
||||
|
||||
To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`.
|
||||
|
||||
For example, to create an API route at `/hello-world/:id/name/:name`, create the file `src/api/hello-world/[id]/name/[name]/route.ts` with the following content:
|
||||
|
||||
export const multiplePathHighlights = [
|
||||
["12", "req.params.id", "Access the path parameter `id`"],
|
||||
["13", "req.params.name", "Access the path parameter `name`"]
|
||||
]
|
||||
|
||||
```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights}
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: `[GET] Hello ${
|
||||
req.params.id
|
||||
} - ${req.params.name}!`,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
You access the `id` and `name` path parameters using the `req.params` property.
|
||||
|
||||
---
|
||||
|
||||
## Query Parameters
|
||||
|
||||
You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value.
|
||||
|
||||
For example:
|
||||
|
||||
export const queryHighlights = [
|
||||
["11", "req.query.name", "Access the query parameter `name`"],
|
||||
]
|
||||
|
||||
```ts title="src/api/hello-world/route.ts" highlights={queryHighlights}
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: `Hello ${req.query.name}`,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The value of `req.query.name` is the value passed in `?name=John`, for example.
|
||||
|
||||
---
|
||||
|
||||
## Request Body Parameters
|
||||
|
||||
The Medusa application parses the body of any request having its `Content-Type` header set to `application/json`. The request body parameters are set in the `MedusaRequest`'s `body` property.
|
||||
|
||||
For example:
|
||||
|
||||
export const bodyHighlights = [
|
||||
["11", "HelloWorldReq", "Specify the type of the request body parameters."],
|
||||
["15", "req.body.name", "Access the request body parameter `name`"],
|
||||
]
|
||||
|
||||
```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights}
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
|
||||
type HelloWorldReq = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export const POST = async (
|
||||
req: MedusaRequest<HelloWorldReq>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: `[POST] Hello ${req.body.name}!`,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
In this example, you use the `name` request body parameter to create the message in the returned response.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
The `MedusaRequest` type accepts a type argument that indicates the type of the request body. This is useful for auto-completion and to avoid typing errors.
|
||||
|
||||
</Note>
|
||||
|
||||
To test it out, send the following request to your Medusa application:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/hello-world' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"name": "John"
|
||||
}'
|
||||
```
|
||||
|
||||
This returns the following JSON object:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "[POST] Hello John!"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,182 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Protected Routes`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to create protected routes.
|
||||
|
||||
## What is a Protected Route?
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Default Protected Routes
|
||||
|
||||
Medusa applies an authentication guard on routes starting with `/admin`, including custom API routes.
|
||||
|
||||
Requests to `/admin` must be user-authenticated to access the route.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Refer to the API Reference for [Admin](!api!/admin#authentication) and [Store](!api!/store#authentication) authentication methods.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Protect Custom API Routes
|
||||
|
||||
To protect custom API Routes to only allow authenticated customer or admin users, use the `authenticate` middleware imported from `@medusajs/medusa`.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
[
|
||||
"10",
|
||||
"authenticate",
|
||||
"Only authenticated admin users can access routes starting with `/custom/admin`",
|
||||
],
|
||||
[
|
||||
"14",
|
||||
"authenticate",
|
||||
"Only authenticated customers can access routes starting with `/custom/customers`",
|
||||
],
|
||||
]
|
||||
|
||||
```ts title="src/api/middlewares.ts" highlights={highlights}
|
||||
import {
|
||||
defineMiddlewares,
|
||||
authenticate,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export default defineMiddlewares({
|
||||
routes: [
|
||||
{
|
||||
matcher: "/custom/admin*",
|
||||
middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
|
||||
},
|
||||
{
|
||||
matcher: "/custom/customer*",
|
||||
middlewares: [authenticate("customer", ["session", "bearer"])],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
The `authenticate` middleware function accepts three parameters:
|
||||
|
||||
1. The type of user authenticating. Use `user` for authenticating admin users, and `customer` for authenticating customers. You can also pass `*` to allow all types of users.
|
||||
2. An array of the types of authentication methods allowed. Both `user` and `customer` scopes support `session` and `bearer`. The `admin` scope also supports the `api-key` authentication method.
|
||||
3. An optional object of configurations accepting the following property:
|
||||
- `allowUnauthenticated`: (default: `false`) A boolean indicating whether authentication is required. For example, you may have an API route where you want to access the logged-in customer if available, but guest customers can still access it too.
|
||||
|
||||
---
|
||||
|
||||
## Authentication Opt-Out
|
||||
|
||||
To disable the authentication guard on custom routes under the `/admin` path prefix, export an `AUTHENTICATE` variable in the route file with its value set to `false`.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/admin/custom/route.ts" highlights={[["15"]]}
|
||||
import type {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: "Hello",
|
||||
})
|
||||
}
|
||||
|
||||
export const AUTHENTICATE = false
|
||||
```
|
||||
|
||||
Now, any request sent to the `/admin/custom` API route is allowed, regardless if the admin user is authenticated.
|
||||
|
||||
---
|
||||
|
||||
## Authenticated Request Type
|
||||
|
||||
To access the authentication details in an API route, such as the logged-in user's ID, set the type of the first request parameter to `AuthenticatedMedusaRequest`. It extends `MedusaRequest`.
|
||||
|
||||
The `auth_context.actor_id` property of `AuthenticatedMedusaRequest` holds the ID of the authenticated user or customer. If there isn't any authenticated user or customer, `auth_context` is `undefined`.
|
||||
|
||||
<Note>
|
||||
|
||||
If you opt-out of authentication in a route as mentioned in the [previous section](#authentication-opt-out), you can't access the authenticated user or customer anymore. Use the [authenticate middleware](#protect-custom-api-routes) instead.
|
||||
|
||||
</Note>
|
||||
|
||||
### Retrieve Logged-In Customer's Details
|
||||
|
||||
You can access the logged-in customer’s ID in all API routes starting with `/store` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/store/custom/route.ts" highlights={[["19", "req.auth_context.actor_id", "Access the logged-in customer's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import { ICustomerModuleService } from "@medusajs/framework/types"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
if (req.auth_context?.actor_id) {
|
||||
// retrieve customer
|
||||
const customerModuleService: ICustomerModuleService = req.scope.resolve(
|
||||
Modules.CUSTOMER
|
||||
)
|
||||
|
||||
const customer = await customerModuleService.retrieveCustomer(
|
||||
req.auth_context.actor_id
|
||||
)
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
In this example, you resolve the Customer Module's main service, then use it to retrieve the logged-in customer, if available.
|
||||
|
||||
### Retrieve Logged-In Admin User's Details
|
||||
|
||||
You can access the logged-in admin user’s ID in all API Routes starting with `/admin` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/admin/custom/route.ts" highlights={[["17", "req.auth_context.actor_id", "Access the logged-in admin user's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import { IUserModuleService } from "@medusajs/framework/types"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const userModuleService: IUserModuleService = req.scope.resolve(
|
||||
Modules.USER
|
||||
)
|
||||
|
||||
const user = await userModuleService.retrieveUser(
|
||||
req.auth_context.actor_id
|
||||
)
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user.
|
||||
@@ -0,0 +1,121 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} API Route Response`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to send a response in your API route.
|
||||
|
||||
## Send a JSON Response
|
||||
|
||||
To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler.
|
||||
|
||||
For example:
|
||||
|
||||
export const jsonHighlights = [
|
||||
["7", "json", "Return a JSON object."]
|
||||
]
|
||||
|
||||
```ts title="src/api/custom/route.ts" highlights={jsonHighlights}
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: "Hello, World!",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This API route returns the following JSON object:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Hello, World!"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Set Response Status Code
|
||||
|
||||
By default, setting the JSON data using the `json` method returns a response with a `200` status code.
|
||||
|
||||
To change the status code, use the `status` method of the `MedusaResponse` object.
|
||||
|
||||
For example:
|
||||
|
||||
export const statusHighlight = [
|
||||
["7", "status", "Set the response code to `201`."]
|
||||
]
|
||||
|
||||
```ts title="src/api/custom/route.ts" highlights={statusHighlight}
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.status(201).json({
|
||||
message: "Hello, World!",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The response of this API route has the status code `201`.
|
||||
|
||||
---
|
||||
|
||||
## Change Response Content Type
|
||||
|
||||
To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type.
|
||||
|
||||
For example, to create an API route that returns an event stream:
|
||||
|
||||
export const streamHighlights = [
|
||||
["7", "writeHead", "Set the response's headers."],
|
||||
["7", "200", "Set the status code."],
|
||||
["8", `"Content-Type"`, "Set the response's content type."],
|
||||
["13", "interval", "Simulate stream data using an interval"],
|
||||
["14", "write", "Write stream data."],
|
||||
["17", "on", "Stop the stream when the request is terminated."]
|
||||
]
|
||||
|
||||
```ts highlights={streamHighlights}
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
})
|
||||
|
||||
const interval = setInterval(() => {
|
||||
res.write("Streaming data...\n")
|
||||
}, 3000)
|
||||
|
||||
req.on("end", () => {
|
||||
clearInterval(interval)
|
||||
res.end()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The `writeHead` method accepts two parameters:
|
||||
|
||||
1. The first one is the response's status code.
|
||||
2. The second is an object of key-value pairs to set the headers of the response.
|
||||
|
||||
This API route opens a stream by setting the `Content-Type` in the header to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds.
|
||||
|
||||
---
|
||||
|
||||
## Do More with Responses
|
||||
|
||||
The `MedusaResponse` type is based on [Express's Response](https://expressjs.com/en/api.html#res). Refer to their API reference for other uses of responses.
|
||||
@@ -0,0 +1,135 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Request Body Parameter Validation`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to validate request body parameters in your custom API route.
|
||||
|
||||
## Example Scenario
|
||||
|
||||
Consider you're creating a `POST` API route at `/custom`. It accepts two paramters `a` and `b` that are required numbers, and returns their sum.
|
||||
|
||||
The next steps explain how to add validation to this API route, as an example.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create Zod Schema
|
||||
|
||||
Medusa uses [Zod](https://zod.dev/) to validate the body parameters of an incoming request.
|
||||
|
||||
To use Zod to validate your custom schemas, create a `validators.ts` file in any `src/api` subfolder. This file holds Zod schemas for each of your API routes.
|
||||
|
||||
For example, create the file `src/api/custom/validators.ts` with the following content:
|
||||
|
||||
```ts title="src/api/custom/validators.ts"
|
||||
import { z } from "zod"
|
||||
|
||||
export const PostStoreCustomSchema = z.object({
|
||||
a: z.number(),
|
||||
b: z.number(),
|
||||
})
|
||||
```
|
||||
|
||||
The `PostStoreCustomSchema` variable is a Zod schema that indicates the request body is valid if:
|
||||
|
||||
1. It's an object.
|
||||
2. It has a property `a` that is a required number.
|
||||
3. It has a property `b` that is a required number.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Add Validation Middleware
|
||||
|
||||
To use this schema for validating the body parameters of requests to `/custom`, use the `validateAndTransformBody` middleware provided by `@medusajs/framework/utils`. It accepts the Zod schema as a parameter.
|
||||
|
||||
For example, create the file `src/api/middlewares.ts` with the following content:
|
||||
|
||||
```ts title="src/api/middlewares.ts"
|
||||
import { defineMiddlewares } from "@medusajs/medusa"
|
||||
import {
|
||||
validateAndTransformBody,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { PostStoreCustomSchema } from "./custom/validators"
|
||||
|
||||
export default defineMiddlewares({
|
||||
routes: [
|
||||
{
|
||||
matcher: "/custom",
|
||||
method: "POST",
|
||||
middlewares: [
|
||||
validateAndTransformBody(PostStoreCustomSchema),
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
This applies the `validateAndTransformBody` middleware on `POST` requests to `/custom`. It uses the `PostStoreCustomSchema` as the validation schema.
|
||||
|
||||
### How the Validation Works
|
||||
|
||||
If a request's body parameters don't pass the validation, the `validateAndTransformBody` middleware throws an error indicating the validation errors.
|
||||
|
||||
If a request's body parameters are validated successfully, the middleware sets the validated body parameters in the `validatedBody` property of `MedusaRequest`.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Use Validated Body in API Route
|
||||
|
||||
In your API route, consume the validated body using the `validatedBody` property of `MedusaRequest`.
|
||||
|
||||
For example, create the file `src/api/custom/route.ts` with the following content:
|
||||
|
||||
export const routeHighlights = [
|
||||
["5", "PostStoreCustomSchemaType", "Infer the request body type from the schema to pass it as a type parameter to `MedusaRequest`."],
|
||||
["14", "", "Access the body parameters using `validatedBody`."]
|
||||
]
|
||||
|
||||
```ts title="src/api/custom/route.ts" highlights={routeHighlights}
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import { z } from "zod"
|
||||
import { PostStoreCustomSchema } from "./validators"
|
||||
|
||||
type PostStoreCustomSchemaType = z.infer<
|
||||
typeof PostStoreCustomSchema
|
||||
>
|
||||
|
||||
export const POST = async (
|
||||
req: MedusaRequest<PostStoreCustomSchemaType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
sum: req.validatedBody.a + req.validatedBody.b,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
In the API route, you use the `validatedBody` property of `MedusaRequest` to access the values of the `a` and `b` properties.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
To pass the request body's type as a type parameter to `MedusaRequest`, use Zod's `infer` type that accepts the type of a schema as a parameter.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test out the validation, send a `POST` request to `/custom`. You can try sending incorrect request body parameters.
|
||||
|
||||
For example, if you omit the `a` parameter, you'll receive a `400` response code with the following response data:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "invalid_data",
|
||||
"message": "Invalid request: Field 'a' is required"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Learn More About Validation Schemas
|
||||
|
||||
To see different examples and learn more about creating a validation schema, refer to [Zod's documentation](https://zod.dev).
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Architectural Modules`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about architectural modules.
|
||||
|
||||
## What is an Architectural Module?
|
||||
|
||||
An architectural module implements features and mechanisms related to the Medusa application’s architecture and infrastructure.
|
||||
|
||||
Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis.
|
||||
|
||||
---
|
||||
|
||||
## Architectural Module Types
|
||||
|
||||
There are different architectural module types including:
|
||||
|
||||

|
||||
|
||||
- Cache Module: Defines the caching mechanism or logic to cache computational results.
|
||||
- Event Module: Integrates a pub/sub service to handle subscribing to and emitting events.
|
||||
- Workflow Engine Module: Integrates a service to store and track workflow executions and steps.
|
||||
- File Module: Integrates a storage service to handle uploading and managing files.
|
||||
- Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers.
|
||||
|
||||
---
|
||||
|
||||
## Architectural Modules List
|
||||
|
||||
Refer to the [Architectural Modules reference](!resources!/architectural-modules) for a list of Medusa’s architectural modules, available modules to install, and how to create an architectural module.
|
||||
@@ -0,0 +1,58 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Medusa's Architecture`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about the architectural layers in Medusa.
|
||||
|
||||
## HTTP, Workflow, and Module Layers
|
||||
|
||||
Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes.
|
||||
|
||||
In a common Medusa application, requests go through four layers in the stack. In order of entry, those are:
|
||||
|
||||
1. API Routes (HTTP): Our API Routes are the typical entry point.
|
||||
2. Workflows: API Routes consume workflows that hold the opinionated business logic of your application.
|
||||
3. Modules: Workflows use domain-specific modules for resource management.
|
||||
3. Data store: Modules query the underlying datastore, which is a PostgreSQL database in common cases.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Database Layer
|
||||
|
||||
The Medusa application injects into each module a connection to the configured PostgreSQL database.
|
||||
|
||||
Modules use that connection to read and write data to the database.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Service Integrations
|
||||
|
||||
Third-party services are integrated through commerce and architectural modules.
|
||||
|
||||
You also create custom third-party integrations through a custom module.
|
||||
|
||||
### Commerce Modules
|
||||
|
||||
Commerce modules integrate third-party services relevant for commerce or user-facing features. For example, you integrate Stripe through a payment module provider.
|
||||
|
||||

|
||||
|
||||
### Architectural Modules
|
||||
|
||||
Architectural modules integrate third-party services and systems for architectural features. For example, you integrate Redis as a pub/sub service to send events, or SendGrid to send notifications.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Full Diagram of Medusa's Architecture
|
||||
|
||||
The following diagram illustrates Medusa's architecture over the three layers.
|
||||
|
||||

|
||||
@@ -0,0 +1,72 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Custom CLI Scripts`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how create and execute custom scripts from Medusa's CLI tool.
|
||||
|
||||
## What is a Custom CLI Script?
|
||||
|
||||
A custom CLI script is a function to execute through Medusa's CLI tool. This is useful when creating custom Medusa tooling to run through the CLI.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Custom CLI Script?
|
||||
|
||||
To create a custom CLI script, create a TypeScript or JavaScript file under the `src/scripts` directory. The file must default export a function.
|
||||
|
||||
For example, create the file `src/scripts/my-script.ts` with the following content:
|
||||
|
||||
```ts title="src/scripts/my-script.ts"
|
||||
import {
|
||||
ExecArgs,
|
||||
IProductModuleService,
|
||||
} from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
export default async function myScript({ container }: ExecArgs) {
|
||||
const productModuleService: IProductModuleService = container.resolve(
|
||||
Modules.PRODUCT
|
||||
)
|
||||
|
||||
const [, count] = await productModuleService
|
||||
.listAndCountProducts()
|
||||
|
||||
console.log(`You have ${count} product(s)`)
|
||||
}
|
||||
```
|
||||
|
||||
The function receives as a parameter an object having a `container` property, which is an instance of the Medusa Container. Use it to resolve resources in your Medusa application.
|
||||
|
||||
---
|
||||
|
||||
## How to Run Custom CLI Script?
|
||||
|
||||
To run the custom CLI script, run the Medusa CLI's `exec` command:
|
||||
|
||||
```bash
|
||||
npx medusa exec ./src/scripts/my-script.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom CLI Script Arguments
|
||||
|
||||
Your script can accept arguments from the command line. Arguments are passed to the function's object parameter in the `args` property.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { ExecArgs } from "@medusajs/framework/types"
|
||||
|
||||
export default async function myScript({ args }: ExecArgs) {
|
||||
console.log(`The arguments you passed: ${args}`)
|
||||
}
|
||||
```
|
||||
|
||||
Then, pass the arguments in the `exec` command after the file path:
|
||||
|
||||
```bash
|
||||
npx medusa exec ./src/scripts/my-script.ts arg1 arg2
|
||||
```
|
||||
@@ -0,0 +1,200 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Seed Data with Custom CLI Script`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to seed data using a custom CLI script.
|
||||
|
||||
## How to Seed Data
|
||||
|
||||
To seed dummy data for development or demo purposes, use a custom CLI script.
|
||||
|
||||
In the CLI script, use your custom workflows or Medusa's existing workflows, which you can browse in [this reference](!resources!/medusa-workflows-reference), to seed data.
|
||||
|
||||
### Example: Seed Dummy Products
|
||||
|
||||
In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products.
|
||||
|
||||
First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script:
|
||||
|
||||
```bash npm2yarn
|
||||
npm install --save-dev @faker-js/faker
|
||||
```
|
||||
|
||||
Then, create the file `src/scripts/demo-products.ts` with the following content:
|
||||
|
||||
export const highlights = [
|
||||
["16", "salesChannelModuleService", "Resolve the Sales Chanel Module's main service"],
|
||||
["19", "logger", "Resolve the logger to log messages in the terminal."],
|
||||
["22", "query", "Resolve Query to retrieve data later."],
|
||||
["26", "defaultSalesChannel", "Retrieve the default sales channel to associate products with."],
|
||||
["31", "sizeOptions", "Declare the size options to be used in the products' variants."],
|
||||
["32", "colorOptions", "Declare the color options to be used in the products' variants."],
|
||||
["33", "currency_code", "Declare the currency code to use in products' prices."],
|
||||
["34", "productsNum", "The number of products to seed."]
|
||||
]
|
||||
|
||||
```ts title="src/scripts/demo-products.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports"
|
||||
import { ExecArgs } from "@medusajs/framework/types"
|
||||
import { faker } from "@faker-js/faker"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
Modules,
|
||||
ProductStatus,
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
createInventoryLevelsWorkflow,
|
||||
createProductsWorkflow,
|
||||
} from "@medusajs/medusa/core-flows"
|
||||
|
||||
export default async function seedDummyProducts({
|
||||
container,
|
||||
}: ExecArgs) {
|
||||
const salesChannelModuleService = container.resolve(
|
||||
Modules.SALES_CHANNEL
|
||||
)
|
||||
const logger = container.resolve(
|
||||
ContainerRegistrationKeys.LOGGER
|
||||
)
|
||||
const query = container.resolve(
|
||||
ContainerRegistrationKeys.QUERY
|
||||
)
|
||||
|
||||
const defaultSalesChannel = await salesChannelModuleService
|
||||
.listSalesChannels({
|
||||
name: "Default Sales Channel",
|
||||
})
|
||||
|
||||
const sizeOptions = ["S", "M", "L", "XL"]
|
||||
const colorOptions = ["Black", "White"]
|
||||
const currency_code = "eur"
|
||||
const productsNum = 50
|
||||
|
||||
// TODO seed products
|
||||
}
|
||||
```
|
||||
|
||||
So far, in the script, you:
|
||||
|
||||
- Resolve the Sales Channel Module's main service to retrieve the application's default sales channel. This is the sales channel the dummy products will be available in.
|
||||
- Resolve the Logger to log messages in the terminal, and Query to later retrieve data useful for the seeded products.
|
||||
- Initialize some default data to use when seeding the products next.
|
||||
|
||||
Next, replace the `TODO` with the following:
|
||||
|
||||
```ts title="src/scripts/demo-products.ts"
|
||||
const productsData = new Array(productsNum).fill(0).map((_, index) => {
|
||||
const title = faker.commerce.product() + "_" + index
|
||||
return {
|
||||
title,
|
||||
is_giftcard: true,
|
||||
description: faker.commerce.productDescription(),
|
||||
status: ProductStatus.PUBLISHED,
|
||||
options: [
|
||||
{
|
||||
title: "Size",
|
||||
values: sizeOptions,
|
||||
},
|
||||
{
|
||||
title: "Color",
|
||||
values: colorOptions,
|
||||
},
|
||||
],
|
||||
images: [
|
||||
{
|
||||
url: faker.image.urlPlaceholder({
|
||||
text: title,
|
||||
}),
|
||||
},
|
||||
{
|
||||
url: faker.image.urlPlaceholder({
|
||||
text: title,
|
||||
}),
|
||||
},
|
||||
],
|
||||
variants: new Array(10).fill(0).map((_, variantIndex) => ({
|
||||
title: `${title} ${variantIndex}`,
|
||||
sku: `variant-${variantIndex}${index}`,
|
||||
prices: new Array(10).fill(0).map((_, priceIndex) => ({
|
||||
currency_code,
|
||||
amount: 10 * priceIndex,
|
||||
})),
|
||||
options: {
|
||||
Size: sizeOptions[Math.floor(Math.random() * 3)],
|
||||
},
|
||||
})),
|
||||
sales_channels: [
|
||||
{
|
||||
id: defaultSalesChannel[0].id,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
// TODO seed products
|
||||
```
|
||||
|
||||
You generate fifty products using the sales channel and variables you initialized, and using Faker for random data, such as the product's title or images.
|
||||
|
||||
Then, replace the new `TODO` with the following:
|
||||
|
||||
```ts title="src/scripts/demo-products.ts"
|
||||
const { result: products } = await createProductsWorkflow(container).run({
|
||||
input: {
|
||||
products: productsData,
|
||||
},
|
||||
})
|
||||
|
||||
logger.info(`Seeded ${products.length} products.`)
|
||||
|
||||
// TODO add inventory levels
|
||||
```
|
||||
|
||||
You create the generated products using the `createProductsWorkflow` imported previously from `@medusajs/medusa/core-flows`. It accepts the product data as input, and returns the created products.
|
||||
|
||||
Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following:
|
||||
|
||||
```ts title="src/scripts/demo-products.ts"
|
||||
logger.info("Seeding inventory levels.")
|
||||
|
||||
const { data: stockLocations } = await query.graph({
|
||||
entity: "stock_location",
|
||||
fields: ["id"],
|
||||
})
|
||||
|
||||
const { data: inventoryItems } = await query.graph({
|
||||
entity: "inventory_item",
|
||||
fields: ["id"],
|
||||
})
|
||||
|
||||
const inventoryLevels = inventoryItems.map((inventoryItem) => ({
|
||||
location_id: stockLocations[0].id,
|
||||
stocked_quantity: 1000000,
|
||||
inventory_item_id: inventoryItem.id,
|
||||
}))
|
||||
|
||||
await createInventoryLevelsWorkflow(container).run({
|
||||
input: {
|
||||
inventory_levels: inventoryLevels,
|
||||
},
|
||||
})
|
||||
|
||||
logger.info("Finished seeding inventory levels data.")
|
||||
```
|
||||
|
||||
You use Query to retrieve the stock location, to use the first location in the application, and the inventory items.
|
||||
|
||||
Then, you generate inventory levels for each inventory item, associating it with the first stock location.
|
||||
|
||||
Finally, you use the `createInventoryLevelsWorkflow` imported from `@medusajs/medusa/core-flows` to create the inventory levels.
|
||||
|
||||
### Test Script
|
||||
|
||||
To test out the script, run the following command in your project's directory:
|
||||
|
||||
```bash
|
||||
npx medusa exec ./src/scripts/demo-products.ts
|
||||
```
|
||||
|
||||
This seeds the products to your database. If you run your Medusa application and view the products in the dashboard, you'll find fifty new products.
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Configure Data Model Properties`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to configure data model properties.
|
||||
|
||||
## Property’s Default Value
|
||||
|
||||
Use the `default` method on a property's definition to specify the default value of a property.
|
||||
|
||||
For example:
|
||||
|
||||
export const defaultHighlights = [
|
||||
["6", "default", "Set the default value to `black`."],
|
||||
["9", "default", "Set the default value to `0`."]
|
||||
]
|
||||
|
||||
```ts highlights={defaultHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
color: model
|
||||
.enum(["black", "white"])
|
||||
.default("black"),
|
||||
age: model
|
||||
.number()
|
||||
.default(0),
|
||||
// ...
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`.
|
||||
|
||||
---
|
||||
|
||||
## Nullable Property
|
||||
|
||||
Use the `nullable` method to indicate that a property’s value can be `null`.
|
||||
|
||||
For example:
|
||||
|
||||
export const nullableHighlights = [
|
||||
["4", "nullable", "Configure the `price` property to allow `null` values."]
|
||||
]
|
||||
|
||||
```ts highlights={nullableHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
price: model.bigNumber().nullable(),
|
||||
// ...
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unique Property
|
||||
|
||||
The `unique` method indicates that a property’s value must be unique in the database through a unique index.
|
||||
|
||||
For example:
|
||||
|
||||
export const uniqueHighlights = [
|
||||
["4", "unique", "Configure the `email` property to allow unique values only."]
|
||||
]
|
||||
|
||||
```ts highlights={uniqueHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const User = model.define("user", {
|
||||
email: model.text().unique(),
|
||||
// ...
|
||||
})
|
||||
|
||||
export default User
|
||||
```
|
||||
|
||||
In this example, multiple users can’t have the same email.
|
||||
@@ -0,0 +1,13 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Data Model Default Properties`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about the properties available by default in your data model.
|
||||
|
||||
When you create a data model, the following properties are created for you by Medusa:
|
||||
|
||||
- `created_at`: A `dateTime` property that stores when a record of the data model was created.
|
||||
- `updated_at`: A `dateTime` property that stores when a record of the data model was updated.
|
||||
- `deleted_at`: A `dateTime` property that stores when a record of the data model was deleted. When you soft-delete a record, Medusa sets the `deleted_at` property to the current date.
|
||||
@@ -0,0 +1,160 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Data Model Database Index`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to define a database index on a data model.
|
||||
|
||||
## Define Database Index on Property
|
||||
|
||||
Use the `index` method on a property's definition to define a database index.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["5", "index", "Define an index on the `name` property."],
|
||||
["6", '"IDX_MY_CUSTOM_NAME"', "Index name is optional."]
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text().index(
|
||||
"IDX_MY_CUSTOM_NAME"
|
||||
),
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
The `index` method optionally accepts the name of the index as a parameter.
|
||||
|
||||
In this example, you define an index on the `name` property.
|
||||
|
||||
---
|
||||
|
||||
## Define Database Index on Data Model
|
||||
|
||||
A data model has an `indexes` method that defines database indices on its properties.
|
||||
|
||||
The index can be on multiple columns (composite index). For example:
|
||||
|
||||
export const dataModelIndexHighlights = [
|
||||
["7", "indexes", "Define indices on the data model's properties."],
|
||||
["9", "on", "Specify the properties to define the index on."]
|
||||
]
|
||||
|
||||
```ts highlights={dataModelIndexHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text(),
|
||||
age: model.number(),
|
||||
}).indexes([
|
||||
{
|
||||
on: ["name", "age"],
|
||||
},
|
||||
])
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
The `indexes` method receives an array of indices as a parameter. Each index is an object with a required `on` property indicating the properties to apply the index on.
|
||||
|
||||
In the above example, you define a composite index on the `name` and `age` properties.
|
||||
|
||||
### Index Conditions
|
||||
|
||||
An index can have conditions. For example:
|
||||
|
||||
export const conditionHighlights = [
|
||||
["10", "where", "Specify conditions on properties."],
|
||||
["11", "", "Create the index when `age` is `30`."]
|
||||
]
|
||||
|
||||
```ts highlights={conditionHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text(),
|
||||
age: model.number(),
|
||||
}).indexes([
|
||||
{
|
||||
on: ["name", "age"],
|
||||
where: {
|
||||
age: 30,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
The index object passed to `indexes` accepts a `where` property whose value is an object of conditions. The object's key is a property's name, and its value is the condition on that property.
|
||||
|
||||
In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`.
|
||||
|
||||
A property's condition can be a negation. For example:
|
||||
|
||||
export const negationHighlights = [
|
||||
["12", "", "Create the index when `age` is not `null`."]
|
||||
]
|
||||
|
||||
```ts highlights={negationHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text(),
|
||||
age: model.number().nullable(),
|
||||
}).indexes([
|
||||
{
|
||||
on: ["name", "age"],
|
||||
where: {
|
||||
age: {
|
||||
$ne: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
A property's value in `where` can be an object having a `$ne` property. `$ne`'s value indicates what the specified property's value shouldn't be.
|
||||
|
||||
In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`.
|
||||
|
||||
### Unique Database Index
|
||||
|
||||
The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index.
|
||||
|
||||
For example:
|
||||
|
||||
export const uniqueHighlights = [
|
||||
["10", "unique", "Specify if the index is a unique index."]
|
||||
]
|
||||
|
||||
```ts highlights={uniqueHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text(),
|
||||
age: model.number(),
|
||||
}).indexes([
|
||||
{
|
||||
on: ["name", "age"],
|
||||
unique: true,
|
||||
},
|
||||
])
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
This creates a unique composite index on the `name` and `age` properties.
|
||||
@@ -0,0 +1,42 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Infer Type of Data Model`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to infer the type of a data model.
|
||||
|
||||
## How to Infer Type of Data Model?
|
||||
|
||||
Consider you have a `MyCustom` data model. You can't reference this data model in a type, such as a workflow input or service method output types, since it's a variable.
|
||||
|
||||
Instead, Medusa provides an `InferTypeOf` utility imported from `@medusajs/framework/types` that transforms your data model to a type.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { InferTypeOf } from "@medusajs/framework/types"
|
||||
import { MyCustom } from "../models/my-custom" // relative path to the model
|
||||
|
||||
export type MyCustom = InferTypeOf<typeof MyCustom>
|
||||
```
|
||||
|
||||
The `InferTypeOf` utility accepts as a type argument the type of the data model.
|
||||
|
||||
Since the `MyCustom` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`.
|
||||
|
||||
You can now use the `MyCustom` type to reference a data model in other types, such as in workflow inputs or service method outputs:
|
||||
|
||||
```ts title="Example Service"
|
||||
// other imports...
|
||||
import { InferTypeOf } from "@medusajs/framework/types"
|
||||
import { MyCustom } from "../models/my-custom"
|
||||
|
||||
type MyCustom = InferTypeOf<typeof MyCustom>
|
||||
|
||||
class HelloModuleService extends MedusaService({ MyCustom }) {
|
||||
async doSomething(): Promise<MyCustom> {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
import { BetaBadge } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Manage Relationships`,
|
||||
}
|
||||
|
||||
# {metadata.title} <BetaBadge tooltipText="Data model relationships are in active development and may change." text="Beta" />
|
||||
|
||||
In this chapter, you'll learn how to manage relationships between data models when creating, updating, or retrieving records using the module's main service.
|
||||
|
||||
## Manage One-to-One Relationship
|
||||
|
||||
### BelongsTo Side of One-to-One
|
||||
|
||||
When you create a record of a data model that belongs to another through a one-to-one relation, pass the ID of the other data model's record in the relation property.
|
||||
|
||||
For example, assuming you have the [User and Email data models from the previous chapter](../relationships/page.mdx#one-to-one-relationship), set an email's user ID as follows:
|
||||
|
||||
export const belongsHighlights = [
|
||||
["4", "user", "The ID of the user the email belongs to."],
|
||||
["11", "user", "The ID of the user the email belongs to."]
|
||||
]
|
||||
|
||||
```ts highlights={belongsHighlights}
|
||||
// when creating an email
|
||||
const email = await helloModuleService.createEmails({
|
||||
// other properties...
|
||||
user: "123",
|
||||
})
|
||||
|
||||
// when updating an email
|
||||
const email = await helloModuleService.updateEmails({
|
||||
id: "321",
|
||||
// other properties...
|
||||
user: "123",
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, you pass the `user` property when creating or updating an email to specify the user it belongs to.
|
||||
|
||||
### HasOne Side
|
||||
|
||||
When you create a record of a data model that has one of another, pass the ID of the other data model's record in the relation property.
|
||||
|
||||
For example, assuming you have the [User and Email data models from the previous chapter](../relationships/page.mdx#one-to-one-relationship), set an user's email ID as follows:
|
||||
|
||||
export const hasOneHighlights = [
|
||||
["4", "email", "The ID of the email that the user has."],
|
||||
["11", "email", "The ID of the email that the user has."]
|
||||
]
|
||||
|
||||
```ts highlights={hasOneHighlights}
|
||||
// when creating a user
|
||||
const user = await helloModuleService.createUsers({
|
||||
// other properties...
|
||||
email: "123",
|
||||
})
|
||||
|
||||
// when updating a user
|
||||
const user = await helloModuleService.updateUsers({
|
||||
id: "321",
|
||||
// other properties...
|
||||
email: "123",
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, you pass the `email` property when creating or updating a user to specify the email it has.
|
||||
|
||||
---
|
||||
|
||||
## Manage One-to-Many Relationship
|
||||
|
||||
In a one-to-many relationship, you can only manage the associations from the `belongsTo` side.
|
||||
|
||||
When you create a record of the data model on the `belongsTo` side, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property.
|
||||
|
||||
For example, assuming you have the [Product and Store data models from the previous chapter](../relationships/page.mdx#one-to-many-relationship), set a product's store ID as follows:
|
||||
|
||||
export const manyBelongsHighlights = [
|
||||
["4", "store_id", "The ID of the store the product belongs to."],
|
||||
["11", "store_id", "The ID of the store the product belongs to."]
|
||||
]
|
||||
|
||||
```ts highlights={manyBelongsHighlights}
|
||||
// when creating a product
|
||||
const product = await helloModuleService.createProducts({
|
||||
// other properties...
|
||||
store_id: "123",
|
||||
})
|
||||
|
||||
// when updating a product
|
||||
const product = await helloModuleService.updateProducts({
|
||||
id: "321",
|
||||
// other properties...
|
||||
store_id: "123",
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to.
|
||||
|
||||
---
|
||||
|
||||
## Manage Many-to-Many Relationship
|
||||
|
||||
### Create Associations
|
||||
|
||||
When you create a record of a data model that has a many-to-many relationship to another data model, pass an array of IDs of the other data model's records in the relation property.
|
||||
|
||||
For example, assuming you have the [Order and Product data models from the previous chapter](../relationships/page.mdx#many-to-many-relationship), set the association between products and orders as follows:
|
||||
|
||||
export const manyHighlights = [
|
||||
["4", "orders", "The IDs of the orders associated with the product."],
|
||||
["11", "products", "The IDs of the products associated with the order."]
|
||||
]
|
||||
|
||||
```ts highlights={manyHighlights}
|
||||
// when creating a product
|
||||
const product = await helloModuleService.createProducts({
|
||||
// other properties...
|
||||
orders: ["123", "321"],
|
||||
})
|
||||
|
||||
// when creating an order
|
||||
const order = await helloModuleService.createOrders({
|
||||
id: "321",
|
||||
// other properties...
|
||||
products: ["123", "321"],
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, you pass the `orders` property when you create a product, and you pass the `products` property when you create an order.
|
||||
|
||||
### Update Associations
|
||||
|
||||
When you use the `update` methods generated by the service factory, you also pass an array of IDs as the relation property's value to add new associated records.
|
||||
|
||||
However, this removes any existing associations to records whose IDs aren't included in the array.
|
||||
|
||||
For example, assuming you have the [Order and Product data models from the previous chapter](../relationships/page.mdx#many-to-many-relationship), you update the product's related orders as so:
|
||||
|
||||
```ts
|
||||
const product = await helloModuleService.updateProducts({
|
||||
id: "123",
|
||||
// other properties...
|
||||
orders: ["321"],
|
||||
})
|
||||
```
|
||||
|
||||
If the product was associated with an order, and you don't include that order's ID in the `orders` array, the association between the product and order is removed.
|
||||
|
||||
So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product:
|
||||
|
||||
export const updateAssociationHighlights = [
|
||||
["1", "retrieveProduct", "Retrieve the product with its orders."],
|
||||
["12", "", "Pass the IDs of the orders previously associated with the product."],
|
||||
["13", "", "Associate the product with a new order."]
|
||||
]
|
||||
|
||||
```ts highlights={updateAssociationHighlights}
|
||||
const product = await helloModuleService.retrieveProduct(
|
||||
"123",
|
||||
{
|
||||
relations: ["orders"],
|
||||
}
|
||||
)
|
||||
|
||||
const updatedProduct = await helloModuleService.updateProducts({
|
||||
id: product.id,
|
||||
// other properties...
|
||||
orders: [
|
||||
...product.orders.map((order) => order.id),
|
||||
"321",
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
This keeps existing associations between the product and orders, and adds a new one.
|
||||
|
||||
---
|
||||
|
||||
## Retrieve Records of Relation
|
||||
|
||||
The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options.
|
||||
|
||||
To retrieve the records associated with a data model's records through a relationship, pass in the second parameter object a `relations` property whose value is an array of relationship names.
|
||||
|
||||
For example, assuming you have the [Order and Product data models from the previous chapter](../relationships/page.mdx#many-to-many-relationship), you retrieve a product's orders as follows:
|
||||
|
||||
export const retrieveHighlights = [
|
||||
["4", `"orders"`, "Retrieve the records associated with the product\nthrough the `orders` relationship."]
|
||||
]
|
||||
|
||||
```ts highlights={retrieveHighlights}
|
||||
const product = await helloModuleService.retrieveProducts(
|
||||
"123",
|
||||
{
|
||||
relations: ["orders"],
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product.
|
||||
@@ -0,0 +1,15 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Data Models Advanced Guides`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you'll learn more about defining data models.
|
||||
|
||||
You'll learn about:
|
||||
|
||||
- The different property types available.
|
||||
- How to set a property as a primary key.
|
||||
- How to create and manage relationships.
|
||||
- How to configure properties, such as making them nullable or searchable.
|
||||
- How to manually write migrations.
|
||||
@@ -0,0 +1,30 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Data Model’s Primary Key`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to configure the primary key of a data model.
|
||||
|
||||
## primaryKey Method
|
||||
|
||||
To set any `id`, `text`, or `number` property as a primary key, use the `primaryKey` method.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["4", "primaryKey", "Define the `id` property to be the data model's primary key."]
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
id: model.id().primaryKey(),
|
||||
// ...
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
In the example above, the `id` property is defined as the data model's primary key.
|
||||
@@ -0,0 +1,204 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Data Model Property Types`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about the types of properties in a data model’s schema.
|
||||
|
||||
These types are available as methods on the `model` utility imported from `@medusajs/framework/utils`.
|
||||
|
||||
## id
|
||||
|
||||
The `id` method defines an automatically generated string ID property. The generated ID is a unique string that has a mix of letters and numbers.
|
||||
|
||||
For example:
|
||||
|
||||
export const idHighlights = [["4", ".id()", "Define an `id` property."]]
|
||||
|
||||
```ts highlights={idHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
id: model.id(),
|
||||
// ...
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## text
|
||||
|
||||
The `text` method defines a string property.
|
||||
|
||||
For example:
|
||||
|
||||
export const textHighlights = [["4", "text", "Define a `text` property."]]
|
||||
|
||||
```ts highlights={textHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
name: model.text(),
|
||||
// ...
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## number
|
||||
|
||||
The `number` method defines a number property.
|
||||
|
||||
For example:
|
||||
|
||||
export const numberHighlights = [["4", "number", "Define a `number` property."]]
|
||||
|
||||
```ts highlights={numberHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
age: model.number(),
|
||||
// ...
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## bigNumber
|
||||
|
||||
The `bigNumber` method defines a number property that expects large numbers, such as prices.
|
||||
|
||||
For example:
|
||||
|
||||
export const bigNumberHighlights = [["4", "bigNumber", "Define a `bigNumber` property."]]
|
||||
|
||||
```ts highlights={bigNumberHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
price: model.bigNumber(),
|
||||
// ...
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## boolean
|
||||
|
||||
The `boolean` method defines a boolean property.
|
||||
|
||||
For example:
|
||||
|
||||
export const booleanHighlights = [["4", "boolean", "Define a `boolean` property."]]
|
||||
|
||||
```ts highlights={booleanHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
hasAccount: model.boolean(),
|
||||
// ...
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### enum
|
||||
|
||||
The `enum` method defines a property whose value can only be one of the specified values.
|
||||
|
||||
For example:
|
||||
|
||||
export const enumHighlights = [["4", "enum", "Define a `enum` property."]]
|
||||
|
||||
```ts highlights={enumHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
color: model.enum(["black", "white"]),
|
||||
// ...
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
The `enum` method accepts an array of possible string values.
|
||||
|
||||
---
|
||||
|
||||
## dateTime
|
||||
|
||||
The `dateTime` method defines a timestamp property.
|
||||
|
||||
For example:
|
||||
|
||||
export const dateTimeHighlights = [["4", "dateTime", "Define a `dateTime` property."]]
|
||||
|
||||
```ts highlights={dateTimeHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
date_of_birth: model.dateTime(),
|
||||
// ...
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## json
|
||||
|
||||
The `json` method defines a property whose value is a stringified JSON object.
|
||||
|
||||
For example:
|
||||
|
||||
export const jsonHighlights = [["4", "json", "Define a `json` property."]]
|
||||
|
||||
```ts highlights={jsonHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
metadata: model.json(),
|
||||
// ...
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## array
|
||||
|
||||
The `array` method defines an array of strings property.
|
||||
|
||||
For example:
|
||||
|
||||
export const arrHightlights = [["4", "array", "Define an `array` property."]]
|
||||
|
||||
```ts highlights={arrHightlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
names: model.array(),
|
||||
// ...
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Properties Reference
|
||||
|
||||
Refer to the [Data Model API reference](https://docs.medusajs.com/v2/resources/references/data-model) for a full reference of the properties.
|
||||
@@ -0,0 +1,252 @@
|
||||
import { BetaBadge } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Data Model Relationships`,
|
||||
}
|
||||
|
||||
# {metadata.title} <BetaBadge text="Beta" tooltipText="Data model relationships are in active development and may change." />
|
||||
|
||||
In this chapter, you’ll learn how to define relationships between data models in your module.
|
||||
|
||||
## What is a Relationship Property?
|
||||
|
||||
A relationship property defines an association in the database between two models. It's created using methods on the `models` utility, such as `hasOne` or `belongsTo`.
|
||||
|
||||
When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type.
|
||||
|
||||
<Note title="Use data model relationships when" type="success">
|
||||
|
||||
You want to create a relation between data models in the same module.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Don't use data model relationships if" type="error">
|
||||
|
||||
You want to create a relationship between data models in different modules. Use module links instead.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## One-to-One Relationship
|
||||
|
||||
A one-to-one relationship indicates that one record of a data model belongs to or is associated with another.
|
||||
|
||||
To define a one-to-one relationship, create relationship properties in the data models using the following methods:
|
||||
|
||||
1. `hasOne`: indicates that the model has one record of the specified model.
|
||||
2. `belongsTo`: indicates that the model belongs to one record of the specified model.
|
||||
|
||||
For example:
|
||||
|
||||
export const oneToOneHighlights = [
|
||||
["5", "hasOne", "A user has one email."],
|
||||
["10", "belongsTo", "An email belongs to a user."],
|
||||
["11", `"email"`, "The relationship's name in the `User` data model."]
|
||||
]
|
||||
|
||||
```ts highlights={oneToOneHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const User = model.define("user", {
|
||||
id: model.id().primaryKey(),
|
||||
email: model.hasOne(() => Email),
|
||||
})
|
||||
|
||||
const Email = model.define("email", {
|
||||
id: model.id().primaryKey(),
|
||||
user: model.belongsTo(() => User, {
|
||||
mappedBy: "email",
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, a user has one email, and an email belongs to one user.
|
||||
|
||||
The `hasOne` and `belongsTo` methods accept a function as a first parameter. The function returns the associated data model.
|
||||
|
||||
The `belongsTo` method also requires passing as a second parameter an object with the property `mappedBy`. Its value is the name of the relationship property in the other data model.
|
||||
|
||||
### Optional Relationship
|
||||
|
||||
To make the relationship optional on the `hasOne` or `belongsTo` side, use the `nullable` method on either properties as explained in [this chapter](../configure-properties/page.mdx#nullable-property).
|
||||
|
||||
### One-to-One Relationship in the Database
|
||||
|
||||
When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property:
|
||||
|
||||
1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column.
|
||||
2. A foreign key on the `{relation_name}_id` column to the table of the related data model.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## One-to-Many Relationship
|
||||
|
||||
A one-to-many relationship indicates that one record of a data model has many records of another data model.
|
||||
|
||||
To define a one-to-many relationship, create relationship properties in the data models using the following methods:
|
||||
|
||||
1. `hasMany`: indicates that the model has more than one records of the specified model.
|
||||
2. `belongsTo`: indicates that the model belongs to one record of the specified model.
|
||||
|
||||
For example:
|
||||
|
||||
export const oneToManyHighlights = [
|
||||
["5", "hasMany", "A store has many products"],
|
||||
["10", "belongsTo", "A product has one store."],
|
||||
["11", `"products"`, "The relationship's name in the `Store` data model."]
|
||||
]
|
||||
|
||||
```ts highlights={oneToManyHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const Store = model.define("store", {
|
||||
id: model.id().primaryKey(),
|
||||
products: model.hasMany(() => Product),
|
||||
})
|
||||
|
||||
const Product = model.define("product", {
|
||||
id: model.id().primaryKey(),
|
||||
store: model.belongsTo(() => Store, {
|
||||
mappedBy: "products",
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
In this example, a store has many products, but a product belongs to one store.
|
||||
|
||||
### Optional Relationship
|
||||
|
||||
To make the relationship optional on the `belongsTo` side, use the `nullable` method on the property as explained in [this chapter](../configure-properties/page.mdx#nullable-property).
|
||||
|
||||
### One-to-Many Relationship in the Database
|
||||
|
||||
When you generate the migrations of data models that have a one-to-many relationship, the migration adds to the table of the data model that has the `belongsTo` property:
|
||||
|
||||
1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `product` table will have a `store_id` column.
|
||||
2. A foreign key on the `{relation_name}_id` column to the table of the related data model.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Many-to-Many Relationship
|
||||
|
||||
A many-to-many relationship indicates that many records of a data model can be associated to many records of another data model.
|
||||
|
||||
To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method.
|
||||
|
||||
For example:
|
||||
|
||||
export const manyToManyHighlights = [
|
||||
["5", "manyToMany", "An order is associated with many products."],
|
||||
["12", "manyToMany", "A product is associated with many orders."]
|
||||
]
|
||||
|
||||
```ts highlights={manyToManyHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const Order = model.define("order", {
|
||||
id: model.id().primaryKey(),
|
||||
products: model.manyToMany(() => Product, {
|
||||
mappedBy: "orders",
|
||||
}),
|
||||
})
|
||||
|
||||
const Product = model.define("product", {
|
||||
id: model.id().primaryKey(),
|
||||
orders: model.manyToMany(() => Order, {
|
||||
mappedBy: "products",
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
At least one side of the many-to-many relationship must have the `mappedBy` property set in the second object parameter of the `manyToMany` object. Its value is the name of the relationship property in the other data model.
|
||||
|
||||
In this example, an order is associated with many products, and a product is associated with many orders.
|
||||
|
||||
### Many-to-Many Relationship in the Database
|
||||
|
||||
When you generate the migrations of data models that have a many-to-many relationship, the migration adds a new pivot table.
|
||||
|
||||
The pivot table has a column with the name `{data_model}_id` for each of the data model's tables. It also has foreign keys on each of these columns to their respective tables.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Set Relationship Name in the Other Model
|
||||
|
||||
The relationship property methods accept as a second parameter an object of options. The `mappedBy` property defines the name of the relationship in the other data model.
|
||||
|
||||
This is useful if the relationship property’s name is different than that of the associated data model.
|
||||
|
||||
As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method.
|
||||
|
||||
For example:
|
||||
|
||||
export const relationNameHighlights = [
|
||||
["6", `"owner"`, "The relationship's name in the `Email` data model."],
|
||||
["13", `"email"`, "The relationship's name in the `User` data model."]
|
||||
]
|
||||
|
||||
```ts highlights={relationNameHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const User = model.define("user", {
|
||||
id: model.id().primaryKey(),
|
||||
email: model.hasOne(() => Email, {
|
||||
mappedBy: "owner",
|
||||
}),
|
||||
})
|
||||
|
||||
const Email = model.define("email", {
|
||||
id: model.id().primaryKey(),
|
||||
owner: model.belongsTo(() => User, {
|
||||
mappedBy: "email",
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
In this example, you specify in the `User` data model’s relationship property that the name of the relationship in the `Email` data model is `owner`.
|
||||
|
||||
---
|
||||
|
||||
## Cascades
|
||||
|
||||
When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it.
|
||||
|
||||
For example, if a store is deleted, its products should also be deleted.
|
||||
|
||||
The `cascades` method used on a data model configures which child records an operation is cascaded to.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["8", "", "When a store is deleted, delete its associated products."]
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const Store = model.define("store", {
|
||||
id: model.id().primaryKey(),
|
||||
products: model.hasMany(() => Product),
|
||||
})
|
||||
.cascades({
|
||||
delete: ["products"],
|
||||
})
|
||||
|
||||
const Product = model.define("product", {
|
||||
id: model.id().primaryKey(),
|
||||
store: model.belongsTo(() => Store, {
|
||||
mappedBy: "products",
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
The `cascades` method accepts an object. Its key is the operation’s name, such as `delete`. The value is an array of relationship property names that the operation is cascaded to.
|
||||
|
||||
In the example above, when a store is deleted, its associated products are also deleted.
|
||||
@@ -0,0 +1,50 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Searchable Data Model Property`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn what a searchable property is and how to define it.
|
||||
|
||||
## What is a Searchable Property?
|
||||
|
||||
Methods generated by the [service factory](../../modules/service-factory/page.mdx) that accept filters, such as `list{ModelName}s`, accept a `q` property as part of the filters.
|
||||
|
||||
When the `q` filter is passed, the data model's searchable properties are queried to find matching records.
|
||||
|
||||
---
|
||||
|
||||
## Define a Searchable Property
|
||||
|
||||
Use the `searchable` method on a `text` property to indicate that it's searchable.
|
||||
|
||||
For example:
|
||||
|
||||
export const searchableHighlights = [
|
||||
["4", "searchable", "Define the `name` property as searchable."]
|
||||
]
|
||||
|
||||
```ts highlights={searchableHighlights}
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
name: model.text().searchable(),
|
||||
// ...
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
In this example, the `name` property is searchable.
|
||||
|
||||
### Search Example
|
||||
|
||||
If you pass a `q` filter to the `listMyCustoms` method:
|
||||
|
||||
```ts
|
||||
const myCustoms = await helloModuleService.listMyCustoms({
|
||||
q: "John",
|
||||
})
|
||||
```
|
||||
|
||||
This retrieves records that include `John` in their `name` property.
|
||||
@@ -0,0 +1,70 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Write Migration`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to create a migration and write it manually.
|
||||
|
||||
## What is a Migration?
|
||||
|
||||
A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It has two methods:
|
||||
|
||||
- The `up` method reflects changes on the database.
|
||||
- The `down` method reverts the changes made in the `up` method.
|
||||
|
||||
---
|
||||
|
||||
## How to Write a Migration?
|
||||
|
||||
The Medusa CLI tool provides a [db:generate](!resources!/medusa-cli/commands/db#dbgenerate) command to generate a migration for the specified modules' data models.
|
||||
|
||||
Alternatively, you can manually create a migration file under the `migrations` directory of your module.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/modules/hello/migrations/Migration20240429.ts"
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20240702105919 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql("create table if not exists \"my_custom\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"my_custom_pkey\" primary key (\"id\"));")
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql("drop table if exists \"my_custom\" cascade;")
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
The migration's file name should be of the format `Migration{YEAR}{MONTH}{DAY}.ts`. The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`.
|
||||
|
||||
In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax.
|
||||
|
||||
In the example above, the `up` method creates the table `my_custom`, and the `down` method drops the table if the migration is reverted.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Run the Migration
|
||||
|
||||
To run your migration, run the following command:
|
||||
|
||||
<Note>
|
||||
|
||||
This command also syncs module links. If you don't want that, use the `--skip-links` option.
|
||||
|
||||
</Note>
|
||||
|
||||
```bash
|
||||
npx medusa db:migrate
|
||||
```
|
||||
|
||||
This reflects the changes in the database as implemented in the migration's `up` method.
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { TypeList } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Event Data Payload`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how subscribers receive an event's data payload.
|
||||
|
||||
## Access Event's Data Payload
|
||||
|
||||
When events are emitted, they’re emitted with a data payload.
|
||||
|
||||
The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["7", "event", "The event details."],
|
||||
["8", "{ id: string }", "The type of expected data payloads."],
|
||||
]
|
||||
|
||||
```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
SubscriberArgs,
|
||||
SubscriberConfig,
|
||||
} from "@medusajs/framework"
|
||||
|
||||
export default async function productCreateHandler({
|
||||
event,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const productId = event.data.id
|
||||
console.log(`The product ${productId} was created`)
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: "product.created",
|
||||
}
|
||||
```
|
||||
|
||||
The `event` object has the following properties:
|
||||
|
||||
<TypeList types={[
|
||||
{
|
||||
name: "data",
|
||||
type: "`object`",
|
||||
description: "The data payload of the event. Its properties are different for each event."
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
type: "string",
|
||||
description: "The name of the triggered event."
|
||||
},
|
||||
{
|
||||
name: "metadata",
|
||||
type: "`object`",
|
||||
description: "Additional data and context of the emitted event.",
|
||||
optional: true
|
||||
},
|
||||
]} sectionTitle="Access Event's Data Payload" />
|
||||
|
||||
This logs the product ID received in the `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. */}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Emit an Event`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to emit an event in a workflow.
|
||||
|
||||
## Emit Event Step
|
||||
|
||||
Medusa provides an `emitEventStep` helper step in the `@medusajs/medusa/core-flows` package that emits an event.
|
||||
|
||||
When you emit an event, you specify the event's name and data payload to pass with the event.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["13", "emitEventStep", "Emit an event."],
|
||||
["14", `"custom.created"`, "The name of the event to emit."],
|
||||
["15", "data", "The data payload to pass with the event."]
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
import {
|
||||
createWorkflow,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
emitEventStep,
|
||||
} from "@medusajs/medusa/core-flows"
|
||||
|
||||
const helloWorldWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
() => {
|
||||
// ...
|
||||
|
||||
emitEventStep({
|
||||
eventName: "custom.created",
|
||||
data: {
|
||||
id: "123",
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this example, you emit the event `custom.created` and pass in the data payload an ID property.
|
||||
|
||||
If you execute the workflow, the emit is emitted and any subscribers listening to the event are executed.
|
||||
@@ -0,0 +1,174 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Add Columns to a Link`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to add custom columns to a link definition and manage them.
|
||||
|
||||
## How to Add Custom Columns to a Link's Table?
|
||||
|
||||
The `defineLink` function used to define a link accepts a third paramter, which is an object of options.
|
||||
|
||||
To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property:
|
||||
|
||||
export const linkHighlights = [
|
||||
["10", "extraColumns", "Custom columns to add to the created link's table."],
|
||||
["11", "metadata", "The column's name."],
|
||||
["12", "type", "The column's type."]
|
||||
]
|
||||
|
||||
```ts highlights={linkHighlights}
|
||||
import HelloModule from "../modules/hello"
|
||||
import ProductModule from "@medusajs/medusa/product"
|
||||
import { defineLink } from "@medusajs/framework/utils"
|
||||
|
||||
export default defineLink(
|
||||
ProductModule.linkable.product,
|
||||
HelloModule.linkable.myCustom,
|
||||
{
|
||||
database: {
|
||||
extraColumns: {
|
||||
metadata: {
|
||||
type: "json",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This adds to the table created for the link between `product` and `myCustom` a `metadata` column of type `json`.
|
||||
|
||||
### Database Options
|
||||
|
||||
The `database` property defines configuration for the table created in the database.
|
||||
|
||||
Its `extraColumns` property defines custom columns to create in the link's table.
|
||||
|
||||
`extraColumns`'s value is an object whose keys are the names of the columns, and values are the column's configurations as an object.
|
||||
|
||||
### Column Configurations
|
||||
|
||||
The column's configurations object accepts the following properties:
|
||||
|
||||
- `type`: The column's type. Possible values are:
|
||||
- `string`
|
||||
- `text`
|
||||
- `integer`
|
||||
- `boolean`
|
||||
- `date`
|
||||
- `time`
|
||||
- `datetime`
|
||||
- `enum`
|
||||
- `json`
|
||||
- `array`
|
||||
- `enumArray`
|
||||
- `float`
|
||||
- `double`
|
||||
- `decimal`
|
||||
- `bigint`
|
||||
- `mediumint`
|
||||
- `smallint`
|
||||
- `tinyint`
|
||||
- `blob`
|
||||
- `uuid`
|
||||
- `uint8array`
|
||||
- `defaultValue`: The column's default value.
|
||||
- `nullable`: Whether the column can have `null` values.
|
||||
|
||||
---
|
||||
|
||||
## Set Custom Column when Creating Link
|
||||
|
||||
The object you pass to the remote link's `create` method accepts a `data` property. Its value is an object whose keys are custom column names, and values are the value of the custom column for this link.
|
||||
|
||||
For example:
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about the remote link, how to resolve it, and its methods in [this chapter](../remote-link/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
```ts
|
||||
await remoteLink.create({
|
||||
[Modules.PRODUCT]: {
|
||||
product_id: "123",
|
||||
},
|
||||
HELLO_MODULE: {
|
||||
my_custom_id: "321",
|
||||
},
|
||||
data: {
|
||||
metadata: {
|
||||
test: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Retrieve Custom Column with Link
|
||||
|
||||
To retrieve linked records with their custom columns, use Query and pass the link definition as the `entity` property's value.
|
||||
|
||||
For example:
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about Query and how to resolve use it [this chapter](../remote-link/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
export const retrieveHighlights = [
|
||||
["1", "productHelloLink", "Import the exported link definition."],
|
||||
["6", "entity", "Pass the link definition to retrieve its data."],
|
||||
["7", `"metadata"`, "Retrieve the `metadata` column."],
|
||||
["7", `"product.*"`, "Retrieve the linked product's details."],
|
||||
["7", `"my_custom.*"`, "Retrieve the linked `myCustom` record's details."],
|
||||
]
|
||||
|
||||
```ts highlights={retrieveHighlights}
|
||||
import productHelloLink from "../links/product-hello"
|
||||
|
||||
// ...
|
||||
|
||||
const { data } = await query.graph({
|
||||
entity: productHelloLink.entryPoint,
|
||||
fields: ["metadata", "product.*", "my_custom.*"],
|
||||
filters: {
|
||||
product_id: "prod_123",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
This retrieves the product of id `prod_123` and its linked `my_custom` records.
|
||||
|
||||
In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link.
|
||||
|
||||
---
|
||||
|
||||
## Update Custom Column's Value
|
||||
|
||||
The remote link's `create` method updates a link's data if the link between the specified records already exists.
|
||||
|
||||
So, to update the value of a custom column in a created link, use the `create` method again passing it a new value for the custom column.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
await remoteLink.create({
|
||||
[Modules.PRODUCT]: {
|
||||
product_id: "123",
|
||||
},
|
||||
HELLO_MODULE: {
|
||||
my_custom_id: "321",
|
||||
},
|
||||
data: {
|
||||
metadata: {
|
||||
test: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Module Link Direction`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case.
|
||||
|
||||
## Link Direction
|
||||
|
||||
The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`.
|
||||
|
||||
For example, the following defines a link from the `helloModuleService`'s `myCustom` data model to the Product Module's `product` data model:
|
||||
|
||||
```ts
|
||||
export default defineLink(
|
||||
HelloModule.linkable.myCustom,
|
||||
ProductModule.linkable.product
|
||||
)
|
||||
```
|
||||
|
||||
Whereas the following defines a link from the Product Module's `product` data model to the `helloModuleService`'s `myCustom` data model:
|
||||
|
||||
```ts
|
||||
export default defineLink(
|
||||
ProductModule.linkable.product,
|
||||
HelloModule.linkable.myCustom
|
||||
)
|
||||
```
|
||||
|
||||
The above links are two different links that serve different purposes.
|
||||
|
||||
---
|
||||
|
||||
## Which Link Direction to Use?
|
||||
|
||||
### Extend Data Models
|
||||
|
||||
If you're adding a link to a data model to extend it and add new fields, define the link from the main data model to the custom data model.
|
||||
|
||||
For example, if the `myCustom` data model adds new fields to the `product` data model, define the link from `product` to `myCustom`:
|
||||
|
||||
```ts
|
||||
export default defineLink(
|
||||
ProductModule.linkable.product,
|
||||
HelloModule.linkable.myCustom
|
||||
)
|
||||
```
|
||||
|
||||
### Associate Data Models
|
||||
|
||||
If you're linking data models to indicate an association between them, define the link from the custom data model to the main data model.
|
||||
|
||||
For example, if the `myCustom` data model is associated to the `product` data model, define the link from `myCustom` to `product`:
|
||||
|
||||
```ts
|
||||
export default defineLink(
|
||||
HelloModule.linkable.myCustom,
|
||||
ProductModule.linkable.product
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,143 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Module Link`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn what a module link is.
|
||||
|
||||
## What is a Module Link?
|
||||
|
||||
Since modules are isolated, you can't access another module's data models to add a relation to it or extend it.
|
||||
|
||||
Instead, you use a module link. A module link forms an association between two data models of different modules, while maintaining module isolation.
|
||||
|
||||
---
|
||||
|
||||
## How to Define a Module Link?
|
||||
|
||||
### 1. Create Link File
|
||||
|
||||
Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines the link using the `defineLink` function imported from `@medusajs/framework/utils` and exports it.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["6", "linkable", "Special `linkable` property that holds the linkable data models of `HelloModule`."],
|
||||
["7", "linkable", "Special `linkable` property that holds the linkable data models of `ProductModule`."],
|
||||
]
|
||||
|
||||
```ts title="src/links/hello-product.ts" highlights={highlights}
|
||||
import HelloModule from "../modules/hello"
|
||||
import ProductModule from "@medusajs/medusa/product"
|
||||
import { defineLink } from "@medusajs/framework/utils"
|
||||
|
||||
export default defineLink(
|
||||
ProductModule.linkable.product,
|
||||
HelloModule.linkable.myCustom
|
||||
)
|
||||
```
|
||||
|
||||
The `defineLink` function accepts as parameters the link configurations of each module's data model. A module has a special `linkable` property that holds these configurations for its data models.
|
||||
|
||||
In this example, you define a module link between the `hello` module's `MyCustom` data model and the Product Module's `Product` data model.
|
||||
|
||||
### 2. Sync Links
|
||||
|
||||
After defining the link, run the `db:sync-links` command:
|
||||
|
||||
```bash
|
||||
npx medusa db:sync-links
|
||||
```
|
||||
|
||||
The Medusa application creates a new table for your link to store the IDs of linked records.
|
||||
|
||||
Use this command whenever you make changes to your links. For example, run this command if you remove your link definition file.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
You can also use the `db:migrate` command, which both runs the migrations and syncs the links.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## How Module Links Work?
|
||||
|
||||
When you define a module link, the Medusa application creates a table in the database for that link.
|
||||
|
||||
Then, when you create links between records of the data models, the IDs of these data models are stored as a new record in the link's table.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## When to Use Module Links
|
||||
|
||||
<Note title="Use module links when" type="success">
|
||||
|
||||
- You want to create a relation between data models from different modules.
|
||||
- You want to extend the data model of another module.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Don't use module links if" type="error">
|
||||
|
||||
You want to create a relationship between data models in the same module. Use data model relationships instead.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Define a List Link
|
||||
|
||||
By default, the defined link establishes a one-to-one relation: a record of a data model is linked to one record of the other data model.
|
||||
|
||||
To specify that a data model can have multiple of its records linked to the other data model's record, use the `isList` option.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import HelloModule from "../modules/hello"
|
||||
import ProductModule from "@medusajs/medusa/product"
|
||||
import { defineLink } from "@medusajs/framework/utils"
|
||||
|
||||
export default defineLink(
|
||||
ProductModule.linkable.product,
|
||||
{
|
||||
linkable: HelloModule.linkable.myCustom,
|
||||
isList: true,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this case, you pass an object of configuration as a parameter instead. The object accepts the following properties:
|
||||
|
||||
- `linkable`: The data model's link configuration.
|
||||
- `isList`: Whether multiple records can be linked to one record of the other data model.
|
||||
|
||||
In this example, a record of `product` can be linked to more than one record of `myCustom`.
|
||||
|
||||
---
|
||||
|
||||
## Set Delete Cascades on Link
|
||||
|
||||
To enable delete cascade on a link so that when a record is deleted, its linked records are also deleted, pass the `deleteCascades` property in the object passed to `defineLink`.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import HelloModule from "../modules/hello"
|
||||
import ProductModule from "@medusajs/medusa/product"
|
||||
import { defineLink } from "@medusajs/framework/utils"
|
||||
|
||||
export default defineLink(
|
||||
ProductModule.linkable.product,
|
||||
{
|
||||
linkable: HelloModule.linkable.myCustom,
|
||||
deleteCascades: true,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this example, when a product is deleted, its linked `myCustom` record is also deleted.
|
||||
@@ -0,0 +1,229 @@
|
||||
import { TypeList, Tabs, TabsList, TabsTriggerVertical, TabsContent, TabsContentWrapper } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Query`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about the Query utility and how to use it to fetch data from modules.
|
||||
|
||||
<Note type="soon" title="In Development">
|
||||
|
||||
Query is in development and is subject to change in future releases.
|
||||
|
||||
</Note>
|
||||
|
||||
## What is Query?
|
||||
|
||||
Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key.
|
||||
|
||||
In your resources, such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s commerce modules.
|
||||
|
||||
---
|
||||
|
||||
## Query Example
|
||||
|
||||
For example, create the route `src/api/query/route.ts` with the following content:
|
||||
|
||||
export const exampleHighlights = [
|
||||
["13", "", "Resolve Query from the Medusa container."],
|
||||
["15", "graph", "Run a query to retrieve data."],
|
||||
["16", "entity", "The name of the data model you're querying."],
|
||||
["17", "fields", "An array of the data model’s properties to retrieve in the result."],
|
||||
]
|
||||
|
||||
```ts title="src/api/query/route.ts" highlights={exampleHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
} from "@medusajs/framework/utils"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
|
||||
|
||||
const { data: myCustoms } = await query.graph({
|
||||
entity: "my_custom",
|
||||
fields: ["id", "name"],
|
||||
})
|
||||
|
||||
res.json({ my_customs: myCustoms })
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key.
|
||||
|
||||
Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties:
|
||||
|
||||
- `entity`: The data model's name, as specified in the first parameter of the `model.define` method used for the data model's definition.
|
||||
- `fields`: An array of the data model’s properties to retrieve in the result.
|
||||
|
||||
The method returns an object that has a `data` property, which holds an array of the retrieved data. For example:
|
||||
|
||||
```json title="Returned Data"
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "123",
|
||||
"name": "test"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Querying the Graph
|
||||
|
||||
When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates.
|
||||
|
||||
This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them.
|
||||
|
||||
---
|
||||
|
||||
## Retrieve Linked Records
|
||||
|
||||
Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`.
|
||||
|
||||
For example:
|
||||
|
||||
```ts highlights={[["6"]]}
|
||||
const { data: myCustoms } = await query.graph({
|
||||
entity: "my_custom",
|
||||
fields: [
|
||||
"id",
|
||||
"name",
|
||||
"product.*",
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
`.*` means that all of data model's properties should be retrieved. To retrieve a specific property, replace the `*` with the property's name. For example, `product.title`.
|
||||
|
||||
</Note>
|
||||
|
||||
### Retrieve List Link Records
|
||||
|
||||
If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`.
|
||||
|
||||
For example:
|
||||
|
||||
```ts highlights={[["6"]]}
|
||||
const { data: myCustoms } = await query.graph({
|
||||
entity: "my_custom",
|
||||
fields: [
|
||||
"id",
|
||||
"name",
|
||||
"products.*",
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Apply Filters
|
||||
|
||||
```ts highlights={[["6"], ["7"], ["8"], ["9"]]}
|
||||
const { data: myCustoms } = await query.graph({
|
||||
entity: "my_custom",
|
||||
fields: ["id", "name"],
|
||||
filters: {
|
||||
id: [
|
||||
"mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
|
||||
"mc_01HWSVWK3KYHKQEE6QGS2JC3FX",
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records.
|
||||
|
||||
In the example above, you filter the `my_custom` records by multiple IDs.
|
||||
|
||||
<Note>
|
||||
|
||||
Filters don't apply on fields of linked data models from other modules.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Sort Records
|
||||
|
||||
```ts highlights={[["5"], ["6"], ["7"]]}
|
||||
const { data: myCustoms } = await query.graph({
|
||||
entity: "my_custom",
|
||||
fields: ["id", "name"],
|
||||
pagination: {
|
||||
order: {
|
||||
name: "DESC",
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
<Note>
|
||||
|
||||
Sorting doesn't work on fields of linked data models from other modules.
|
||||
|
||||
</Note>
|
||||
|
||||
The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records.
|
||||
|
||||
To sort returned records, pass an `order` property to `pagination`.
|
||||
|
||||
The `order` property is an object whose keys are property names, and values are either:
|
||||
|
||||
- `ASC` to sort records by that property in ascending order.
|
||||
- `DESC` to sort records by that property in descending order.
|
||||
|
||||
---
|
||||
|
||||
## Apply Pagination
|
||||
|
||||
```ts highlights={[["8", "skip", "The number of records to skip before fetching the results."], ["9", "take", "The number of records to fetch."]]}
|
||||
const {
|
||||
data: myCustoms,
|
||||
metadata: { count, take, skip },
|
||||
} = await query.graph({
|
||||
entity: "my_custom",
|
||||
fields: ["id", "name"],
|
||||
pagination: {
|
||||
skip: 0,
|
||||
take: 10,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
To paginate the returned records, pass the following properties to `pagination`:
|
||||
|
||||
- `skip`: (required to apply pagination) The number of records to skip before fetching the results.
|
||||
- `take`: The number of records to fetch.
|
||||
|
||||
When you provide the pagination fields, the `query.graph` method's returned object has a `metadata` property. Its value is an object having the following properties:
|
||||
|
||||
<TypeList types={[
|
||||
{
|
||||
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" />
|
||||
@@ -0,0 +1,153 @@
|
||||
import { BetaBadge } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Remote Link`,
|
||||
}
|
||||
|
||||
# {metadata.title} <BetaBadge text="Beta" tooltipText="Remote Links are in active development." />
|
||||
|
||||
In this chapter, you’ll learn what the remote link is and how to use it to manage links.
|
||||
|
||||
## What is the Remote Link?
|
||||
|
||||
The remote link is a class with utility methods to manage links between data models. It’s registered in the Medusa container under the `remoteLink` registration name.
|
||||
|
||||
For example:
|
||||
|
||||
```ts collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
RemoteLink,
|
||||
} from "@medusajs/framework/modules-sdk"
|
||||
|
||||
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/framework/utils"
|
||||
|
||||
// ...
|
||||
|
||||
await remoteLink.create({
|
||||
[Modules.PRODUCT]: {
|
||||
product_id: "prod_123",
|
||||
},
|
||||
"helloModuleService": {
|
||||
my_custom_id: "mc_123",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules.
|
||||
|
||||
<Note title="Important">
|
||||
|
||||
The keys (names of linked modules) must be in the same direction of the link definition.
|
||||
|
||||
</Note>
|
||||
|
||||
The value of each module’s property is an object, whose keys are of the format `{data_model_snake_name}_id`, and values are the IDs of the linked record.
|
||||
|
||||
So, in the example above, you link a record of the `MyCustom` data model in a `hello` module to a `Product` record in the Product Module.
|
||||
|
||||
---
|
||||
|
||||
## Dismiss Link
|
||||
|
||||
To remove a link between records of two data models, use the `dismiss` method of the remote link.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
// ...
|
||||
|
||||
await remoteLink.dismiss({
|
||||
[Modules.PRODUCT]: {
|
||||
product_id: "prod_123",
|
||||
},
|
||||
"helloModuleService": {
|
||||
my_custom_id: "mc_123",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The `dismiss` method accepts the same parameter type as the [create method](#create-link).
|
||||
|
||||
<Note title="Important">
|
||||
|
||||
The keys (names of linked modules) must be in the same direction of the link definition.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Cascade Delete Linked Records
|
||||
|
||||
If a record is deleted, use the `delete` method of the remote link to delete all linked records.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
// ...
|
||||
|
||||
await productModuleService.deleteVariants([variant.id])
|
||||
|
||||
await remoteLink.delete({
|
||||
[Modules.PRODUCT]: {
|
||||
product_id: "prod_123",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
This deletes all records linked to the deleted product.
|
||||
|
||||
---
|
||||
|
||||
## Restore Linked Records
|
||||
|
||||
If a record that was previously soft-deleted is now restored, use the `restore` method of the remote link to restore all linked records.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
// ...
|
||||
|
||||
await productModuleService.restoreProducts(["prod_123"])
|
||||
|
||||
await remoteLink.restore({
|
||||
[Modules.PRODUCT]: {
|
||||
product_id: "prod_123",
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,70 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Module's Container`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about the module's container and how to resolve resources in that container.
|
||||
|
||||
## Module's Container
|
||||
|
||||
Since modules are isolated, 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 other resources registered in the module's container.
|
||||
|
||||
### List of Registered Resources
|
||||
|
||||
Find a list of resources or dependencies registered in a module's container in [this Learning Resources reference](!resoures!/medusa-container-resources).
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
|
||||
```ts highlights={[["4"], ["10"]]}
|
||||
import { Logger } from "@medusajs/framework/types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
export default class HelloModuleService {
|
||||
protected logger_: Logger
|
||||
|
||||
constructor({ logger }: InjectedDependencies) {
|
||||
this.logger_ = logger
|
||||
|
||||
this.logger_.info("[HelloModuleService]: Hello World!")
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Loader
|
||||
|
||||
A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources.
|
||||
|
||||
For example:
|
||||
|
||||
```ts highlights={[["9"]]}
|
||||
import {
|
||||
LoaderOptions,
|
||||
} from "@medusajs/framework/types"
|
||||
import {
|
||||
ContainerRegistrationKeys
|
||||
} from "@medusajs/framework/utils"
|
||||
|
||||
export default async function helloWorldLoader({
|
||||
container,
|
||||
}: LoaderOptions) {
|
||||
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
||||
|
||||
logger.info("[helloWorldLoader]: Hello, World!")
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,464 @@
|
||||
import { CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Perform Database Operations in a Service`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to perform database operations in a module's service.
|
||||
|
||||
<Note>
|
||||
|
||||
This chapter is intended for more advanced database use-cases where you need more control over queries and operations. For basic database operations, such as creating or retrieving data of a model, use the [Service Factory](../service-factory/page.mdx) instead.
|
||||
|
||||
</Note>
|
||||
|
||||
## Run Queries
|
||||
|
||||
[MikroORM's entity manager](https://mikro-orm.io/docs/entity-manager) is a class that has methods to run queries on the database and perform operations.
|
||||
|
||||
Medusa provides an `InjectManager` decorator imported from `@medusajs/utils` that injects a service's method with a [forked entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager).
|
||||
|
||||
So, to run database queries in a service:
|
||||
|
||||
1. Add the `InjectManager` decorator to the method.
|
||||
2. Add as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator imported from `@medusajs/utils`. This context holds database-related context, including the manager injected by `InjectManager`
|
||||
|
||||
For example, in your service, add the following methods:
|
||||
|
||||
export const methodsHighlight = [
|
||||
["11", "getCount", "Retrieves the number of records in `my_custom` using the `count` method."],
|
||||
["18", "getCountSql", "Retrieves the number of records in `my_custom` using the `execute` method."]
|
||||
]
|
||||
|
||||
```ts highlights={methodsHighlight}
|
||||
// other imports...
|
||||
import {
|
||||
InjectManager,
|
||||
MedusaContext,
|
||||
} from "@medusajs/framework/utils"
|
||||
|
||||
class HelloModuleService {
|
||||
// ...
|
||||
|
||||
@InjectManager()
|
||||
async getCount(
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
): Promise<number> {
|
||||
return await sharedContext.manager.count("my_custom")
|
||||
}
|
||||
|
||||
@InjectManager()
|
||||
async getCountSql(
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
): Promise<number> {
|
||||
const data = await sharedContext.manager.execute(
|
||||
"SELECT COUNT(*) as num FROM my_custom"
|
||||
)
|
||||
|
||||
return parseInt(data[0].num)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You add two methods `getCount` and `getCountSql` that have the `InjectManager` decorator. Each of the methods also accept the `sharedContext` parameter which has the `MedusaContext` decorator.
|
||||
|
||||
The entity manager is injected to the `sharedContext.manager` property, which is an instance of [EntityManager from the @mikro-orm/knex package](https://mikro-orm.io/api/5.9/knex/class/EntityManager).
|
||||
|
||||
You use the manager in the `getCount` method to retrieve the number of records in a table, and in the `getCountSql` to run a PostgreSQL query that retrieves the count.
|
||||
|
||||
<Note>
|
||||
|
||||
Refer to [MikroORM's reference](https://mikro-orm.io/api/5.9/knex/class/EntityManager) for a full list of the entity manager's methods.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Execute Operations in Transactions
|
||||
|
||||
To wrap database operations in a transaction, you create two methods:
|
||||
|
||||
1. A private or protected method that's wrapped in a transaction. To wrap it in a transaction, you use the `InjectTransactionManager` decorator imported from `@medusajs/utils`.
|
||||
2. A public method that calls the transactional method. You use on it the `InjectManager` decorator as explained in the previous section.
|
||||
|
||||
Both methods must accept as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator imported from `@medusajs/utils`. It holds database-related contexts passed through the Medusa application.
|
||||
|
||||
For example:
|
||||
|
||||
export const opHighlights = [
|
||||
["11", "InjectTransactionManager", "A decorator that injects the a transactional entity manager into the `sharedContext` parameter."],
|
||||
["17", "MedusaContext", "A decorator to use Medusa's shared context."],
|
||||
["20", "nativeUpdate", "Update a record."],
|
||||
["31", "execute", "Retrieve the updated record."],
|
||||
["38", "InjectManager", "A decorator that injects a forked entity manager into the context."],
|
||||
]
|
||||
|
||||
```ts highlights={opHighlights}
|
||||
import {
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { Context } from "@medusajs/framework/types"
|
||||
import { EntityManager } from "@mikro-orm/knex"
|
||||
|
||||
class HelloModuleService {
|
||||
// ...
|
||||
@InjectTransactionManager()
|
||||
protected async update_(
|
||||
input: {
|
||||
id: string,
|
||||
name: string
|
||||
},
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
): Promise<any> {
|
||||
const transactionManager = sharedContext.transactionManager
|
||||
await transactionManager.nativeUpdate(
|
||||
"my_custom",
|
||||
{
|
||||
id: input.id,
|
||||
},
|
||||
{
|
||||
name: input.name,
|
||||
}
|
||||
)
|
||||
|
||||
// retrieve again
|
||||
const updatedRecord = await transactionManager.execute(
|
||||
`SELECT * FROM my_custom WHERE id = '${input.id}'`
|
||||
)
|
||||
|
||||
return updatedRecord
|
||||
}
|
||||
|
||||
@InjectManager()
|
||||
async update(
|
||||
input: {
|
||||
id: string,
|
||||
name: string
|
||||
},
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
) {
|
||||
return await this.update_(input, sharedContext)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `HelloModuleService` has two methods:
|
||||
|
||||
- A protected `update_` that performs the database operations inside a transaction.
|
||||
- A public `update` that executes the transactional protected method.
|
||||
|
||||
The shared context's `transactionManager` property holds the transactional entity manager (injected by `InjectTransactionManager`) that you use to perform database operations.
|
||||
|
||||
<Note>
|
||||
|
||||
Refer to [MikroORM's reference](https://mikro-orm.io/api/5.9/knex/class/EntityManager) for a full list of the entity manager's methods.
|
||||
|
||||
</Note>
|
||||
|
||||
### Why Wrap a Transactional Method
|
||||
|
||||
The variables in the transactional method (for example, `update_`) hold values that are uncomitted to the database. They're only committed once the method finishes execution.
|
||||
|
||||
So, if in your method you perform database operations, then use their result to perform other actions, such as connect to a third-party service, you'll be working with uncommitted data.
|
||||
|
||||
By placing only the database operations in a method that has the `InjectTransactionManager` and using it in a wrapper method, the wrapper method receives the committed result of the transactional method.
|
||||
|
||||
<Note title="Optimization Tip">
|
||||
|
||||
This is also useful if you perform heavy data normalization outside of the database operations. In that case, you don't hold the transaction for a longer time than needed.
|
||||
|
||||
</Note>
|
||||
|
||||
For example, the `update` method could be changed to the following:
|
||||
|
||||
```ts
|
||||
// other imports...
|
||||
import { EntityManager } from "@mikro-orm/knex"
|
||||
|
||||
class HelloModuleService {
|
||||
// ...
|
||||
@InjectManager()
|
||||
async update(
|
||||
input: {
|
||||
id: string,
|
||||
name: string
|
||||
},
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
) {
|
||||
const newData = await this.update_(input, sharedContext)
|
||||
|
||||
await sendNewDataToSystem(newData)
|
||||
|
||||
return newData
|
||||
}
|
||||
}
|
||||
```
|
||||
In this case, only the `update_` method is wrapped in a transaction. The returned value `newData` holds the committed result, which can be used for other operations, such as passed to a `sendNewDataToSystem` method.
|
||||
|
||||
### Using Methods in Transactional Methods
|
||||
|
||||
If your transactional method uses other methods that accept a Medusa context, pass the shared context to those method.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
// other imports...
|
||||
import { EntityManager } from "@mikro-orm/knex"
|
||||
|
||||
class HelloModuleService {
|
||||
// ...
|
||||
@InjectTransactionManager()
|
||||
protected async anotherMethod(
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
protected async update_(
|
||||
input: {
|
||||
id: string,
|
||||
name: string
|
||||
},
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
): Promise<any> {
|
||||
anotherMethod(sharedContext)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context.
|
||||
|
||||
The `anotherMethod` now runs in the same transaction as the `update_` method.
|
||||
|
||||
---
|
||||
|
||||
## Configure Transactions
|
||||
|
||||
To configure the transaction, such as its [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html), use the `baseRepository` dependency registered in your module's container.
|
||||
|
||||
The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more.
|
||||
|
||||
The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction.
|
||||
|
||||
For example, resolve the `baseRepository` in your service's constructor:
|
||||
|
||||
<CodeTabs group="service-type">
|
||||
<CodeTab label="Extending Service Factory" value="service-factory">
|
||||
|
||||
```ts highlights={[["14"]]}
|
||||
import { MedusaService } from "@medusajs/framework/utils"
|
||||
import MyCustom from "./models/my-custom"
|
||||
import { DAL } from "@medusajs/framework/types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
class HelloModuleService extends MedusaService({
|
||||
MyCustom,
|
||||
}){
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
|
||||
constructor({ baseRepository }: InjectedDependencies) {
|
||||
super(...arguments)
|
||||
this.baseRepository_ = baseRepository
|
||||
}
|
||||
}
|
||||
|
||||
export default HelloModuleService
|
||||
```
|
||||
</CodeTab>
|
||||
<CodeTab label="Without Service Factory" value="no-service-factory">
|
||||
|
||||
```ts highlights={[["10"]]}
|
||||
import { DAL } from "@medusajs/framework/types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
class HelloModuleService {
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
|
||||
constructor({ manager }: InjectedDependencies) {
|
||||
this.baseRepository_ = baseRepository
|
||||
}
|
||||
}
|
||||
|
||||
export default HelloModuleService
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
Then, add the following method that uses it:
|
||||
|
||||
export const repoHighlights = [
|
||||
["20", "transaction", "Wrap the function parameter in a transaction."]
|
||||
]
|
||||
|
||||
```ts highlights={repoHighlights}
|
||||
// ...
|
||||
import {
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { Context } from "@medusajs/framework/types"
|
||||
import { EntityManager } from "@mikro-orm/knex"
|
||||
|
||||
class HelloModuleService {
|
||||
// ...
|
||||
@InjectTransactionManager()
|
||||
protected async update_(
|
||||
input: {
|
||||
id: string,
|
||||
name: string
|
||||
},
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
): Promise<any> {
|
||||
return await this.baseRepository_.transaction(
|
||||
async (transactionManager) => {
|
||||
await transactionManager.nativeUpdate(
|
||||
"my_custom",
|
||||
{
|
||||
id: input.id,
|
||||
},
|
||||
{
|
||||
name: input.name,
|
||||
}
|
||||
)
|
||||
|
||||
// retrieve again
|
||||
const updatedRecord = await transactionManager.execute(
|
||||
`SELECT * FROM my_custom WHERE id = '${input.id}'`
|
||||
)
|
||||
|
||||
return updatedRecord
|
||||
},
|
||||
{
|
||||
transaction: sharedContext.transactionManager,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectManager()
|
||||
async update(
|
||||
input: {
|
||||
id: string,
|
||||
name: string
|
||||
},
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
) {
|
||||
return await this.update_(input, sharedContext)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `update_` method uses the `baseRepository_.transaction` method to wrap a function in a transaction.
|
||||
|
||||
The function parameter receives a transactional entity manager as a parameter. Use it to perform the database operations.
|
||||
|
||||
The `baseRepository_.transaction` method also receives as a second parameter an object of options. You must pass in it the `transaction` property and set its value to the `sharedContext.transactionManager` property so that the function wrapped in the transaction uses the injected transaction manager.
|
||||
|
||||
<Note>
|
||||
|
||||
Refer to [MikroORM's reference](https://mikro-orm.io/api/5.9/knex/class/EntityManager) for a full list of the entity manager's methods.
|
||||
|
||||
</Note>
|
||||
|
||||
### Transaction Options
|
||||
|
||||
The second parameter of the `baseRepository_.transaction` method is an object of options that accepts the following properties:
|
||||
|
||||
1. `transaction`: Set the transactional entity manager passed to the function. You must provide this option as explained in the previous section.
|
||||
|
||||
```ts highlights={[["16"]]}
|
||||
// other imports...
|
||||
import { EntityManager } from "@mikro-orm/knex"
|
||||
|
||||
class HelloModuleService {
|
||||
// ...
|
||||
@InjectTransactionManager()
|
||||
async update_(
|
||||
input: {
|
||||
id: string,
|
||||
name: string
|
||||
},
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
): Promise<any> {
|
||||
return await this.baseRepository_.transaction<EntityManager>(
|
||||
async (transactionManager) => {
|
||||
// ...
|
||||
},
|
||||
{
|
||||
transaction: sharedContext.transactionManager,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. `isolationLevel`: Sets the transaction's [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html). Its values can be:
|
||||
- `read committed`
|
||||
- `read uncommitted`
|
||||
- `snapshot`
|
||||
- `repeatable read`
|
||||
- `serializable`
|
||||
|
||||
```ts highlights={[["19"]]}
|
||||
// other imports...
|
||||
import { IsolationLevel } from "@mikro-orm/core"
|
||||
|
||||
class HelloModuleService {
|
||||
// ...
|
||||
@InjectTransactionManager()
|
||||
async update_(
|
||||
input: {
|
||||
id: string,
|
||||
name: string
|
||||
},
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
): Promise<any> {
|
||||
return await this.baseRepository_.transaction<EntityManager>(
|
||||
async (transactionManager) => {
|
||||
// ...
|
||||
},
|
||||
{
|
||||
isolationLevel: IsolationLevel.READ_COMMITTED,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. `enableNestedTransactions`: (default: `false`) whether to allow using nested transactions.
|
||||
- If `transaction` is provided and this is disabled, the manager in `transaction` is re-used.
|
||||
|
||||
```ts highlights={[["16"]]}
|
||||
class HelloModuleService {
|
||||
// ...
|
||||
@InjectTransactionManager()
|
||||
async update_(
|
||||
input: {
|
||||
id: string,
|
||||
name: string
|
||||
},
|
||||
@MedusaContext() sharedContext?: Context<EntityManager>
|
||||
): Promise<any> {
|
||||
return await this.baseRepository_.transaction<EntityManager>(
|
||||
async (transactionManager) => {
|
||||
// ...
|
||||
},
|
||||
{
|
||||
enableNestedTransactions: false,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,113 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Module Isolation`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how modules are isolated, and what that means for your custom development.
|
||||
|
||||
<Note title="Summary">
|
||||
|
||||
- Modules can't access resources, such as services or data models, from other modules.
|
||||
- Use Medusa's linking concepts, as explained in the [Module Links chapters](../../module-links/page.mdx), to extend a module's data models and retrieve data across modules.
|
||||
|
||||
</Note>
|
||||
|
||||
## How are Modules Isolated?
|
||||
|
||||
A module is unaware of any resources other than its own, such as services or data models. This means it can't access these resources if they're implemented in another module.
|
||||
|
||||
For example, your custom module can't resolve the Product Module's main service or have direct relationships from its data model to the Product Module's data models.
|
||||
|
||||
---
|
||||
|
||||
## Why are Modules Isolated
|
||||
|
||||
Some of the module isolation's benefits include:
|
||||
|
||||
- Integrate your module into any Medusa application without side-effects to your setup.
|
||||
- Replace existing modules with your custom implementation, if your use case is drastically different.
|
||||
- Use modules in other environments, such as Edge functions and Next.js apps.
|
||||
|
||||
---
|
||||
|
||||
## How to Extend Data Model of Another Module?
|
||||
|
||||
To extend the data model of another module, such as the `product` data model of the Product Module, use Medusa's linking concepts as explained in the [Module Links chapters](../../module-links/page.mdx).
|
||||
|
||||
---
|
||||
|
||||
## How to Use Services of Other Modules?
|
||||
|
||||
If you're building a feature that uses functionalities from different modules, use a workflow whose steps resolve the modules' services to perform these functionalities.
|
||||
|
||||
Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output.
|
||||
|
||||
### Example
|
||||
|
||||
For example, consider you have two modules:
|
||||
|
||||
1. A module that stores and manages brands in your application.
|
||||
2. A module that integrates a third-party Content Management System (CMS).
|
||||
|
||||
To sync brands from your application to the third-party system, create the following steps:
|
||||
|
||||
export const stepsHighlights = [
|
||||
["1", "retrieveBrandsStep", "A step that retrieves brands using a brand module."],
|
||||
["14", "createBrandsInCmsStep", "A step that creates brands using a CMS module."],
|
||||
["25", "", "Add a compensation function to the step if an error occurs."]
|
||||
]
|
||||
|
||||
```ts title="Example Steps" highlights={stepsHighlights}
|
||||
const retrieveBrandsStep = createStep(
|
||||
"retrieve-brands",
|
||||
async (_, { container }) => {
|
||||
const brandModuleService = container.resolve(
|
||||
"brandModuleService"
|
||||
)
|
||||
|
||||
const brands = await brandModuleService.listBrands()
|
||||
|
||||
return new StepResponse(brands)
|
||||
}
|
||||
)
|
||||
|
||||
const createBrandsInCmsStep = createStep(
|
||||
"create-brands-in-cms",
|
||||
async ({ brands }, { container }) => {
|
||||
const cmsModuleService = container.resolve(
|
||||
"cmsModuleService"
|
||||
)
|
||||
|
||||
const cmsBrands = await cmsModuleService.createBrands(brands)
|
||||
|
||||
return new StepResponse(cmsBrands, cmsBrands)
|
||||
},
|
||||
async (brands, { container }) => {
|
||||
const cmsModuleService = container.resolve(
|
||||
"cmsModuleService"
|
||||
)
|
||||
|
||||
await cmsModuleService.deleteBrands(
|
||||
brands.map((brand) => brand.id)
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module.
|
||||
|
||||
Then, create the following workflow that uses these steps:
|
||||
|
||||
```ts title="Example Workflow"
|
||||
export const syncBrandsWorkflow = createWorkflow(
|
||||
"sync-brands",
|
||||
() => {
|
||||
const brands = retrieveBrandsStep()
|
||||
|
||||
updateBrandsInCmsStep({ brands })
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
You can then use this workflow in an API route, scheduled job, or other resources that use this functionality.
|
||||
@@ -0,0 +1,130 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Multiple Services in a Module`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to use multiple services in a module.
|
||||
|
||||
## Module's Main and Internal Services
|
||||
|
||||
A module has one main service only, which is the service exported in the module's definition.
|
||||
|
||||
However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources.
|
||||
|
||||
---
|
||||
|
||||
## How to Add an Internal Service
|
||||
|
||||
### 1. Create Service
|
||||
|
||||
To add an internal service, create it in the `services` directory of your module.
|
||||
|
||||
For example, create the file `src/modules/hello/services/client.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/services/client.ts"
|
||||
export class ClientService {
|
||||
async getMessage(): Promise<string> {
|
||||
return "Hello, World!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Export Service in Index
|
||||
|
||||
Next, create an `index.ts` file under the `services` directory of the module that exports your internal services.
|
||||
|
||||
For example, create the file `src/modules/hello/services/index.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/services/index.ts"
|
||||
export * from "./client"
|
||||
```
|
||||
|
||||
This exports the `ClientService`.
|
||||
|
||||
### 3. Resolve Internal Service
|
||||
|
||||
Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders.
|
||||
|
||||
For example, in your main service:
|
||||
|
||||
```ts title="src/modules/hello/service.ts" highlights={[["5"], ["13"]]}
|
||||
// other imports...
|
||||
import { ClientService } from "./services"
|
||||
|
||||
type InjectedDependencies = {
|
||||
clientService: ClientService
|
||||
}
|
||||
|
||||
class HelloModuleService extends MedusaService({
|
||||
MyCustom,
|
||||
}){
|
||||
protected clientService_: ClientService
|
||||
|
||||
constructor({ clientService }: InjectedDependencies) {
|
||||
super(...arguments)
|
||||
this.clientService_ = clientService
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can now use your internal service in your main service.
|
||||
|
||||
---
|
||||
|
||||
## Resolve Resources in Internal Service
|
||||
|
||||
Resolve dependencies from your module's container in the constructor of your internal service.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { Logger } from "@medusajs/framework/types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
export class ClientService {
|
||||
protected logger_: Logger
|
||||
|
||||
constructor({ logger }: InjectedDependencies) {
|
||||
this.logger_ = logger
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Access Module Options
|
||||
|
||||
Your internal service can't access the module's options.
|
||||
|
||||
To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { ConfigModule } from "@medusajs/framework/types"
|
||||
import { HELLO_MODULE } from ".."
|
||||
|
||||
export type InjectedDependencies = {
|
||||
configModule: ConfigModule
|
||||
}
|
||||
|
||||
export class ClientService {
|
||||
protected options: Record<string, any>
|
||||
|
||||
constructor({ configModule }: InjectedDependencies) {
|
||||
const moduleDef = configModule.modules[HELLO_MODULE]
|
||||
|
||||
if (typeof moduleDef !== "boolean") {
|
||||
this.options = moduleDef.options
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key.
|
||||
|
||||
If its value is not a `boolean`, set the service's options to the module configuration's `options` property.
|
||||
@@ -0,0 +1,100 @@
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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.ts`.
|
||||
|
||||
For example:
|
||||
|
||||
```js title="medusa-config.ts"
|
||||
module.exports = defineConfig({
|
||||
// ...
|
||||
modules: [
|
||||
{
|
||||
resolve: "./src/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 options as a second parameter.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/modules/hello/service.ts" highlights={[["12"], ["14", "options?: ModuleOptions"], ["17"], ["18"], ["19"]]}
|
||||
import { MedusaService } from "@medusajs/framework/utils"
|
||||
import MyCustom from "./models/my-custom"
|
||||
|
||||
// recommended to define type in another file
|
||||
type ModuleOptions = {
|
||||
capitalize?: boolean
|
||||
}
|
||||
|
||||
export default class HelloModuleService extends MedusaService({
|
||||
MyCustom,
|
||||
}){
|
||||
protected options_: ModuleOptions
|
||||
|
||||
constructor({}, options?: ModuleOptions) {
|
||||
super(...arguments)
|
||||
|
||||
this.options_ = options || {
|
||||
capitalize: false,
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Access Module Options in Loader
|
||||
|
||||
The object that a module’s loaders receive as a parameter has an `options` property holding the module's options.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/modules/hello/loaders/hello-world.ts" highlights={[["11"], ["12", "ModuleOptions", "The type of expected module options."], ["16"]]}
|
||||
import {
|
||||
LoaderOptions,
|
||||
} from "@medusajs/framework/types"
|
||||
|
||||
// recommended to define type in another file
|
||||
type ModuleOptions = {
|
||||
capitalize?: boolean
|
||||
}
|
||||
|
||||
export default async function helloWorldLoader({
|
||||
options,
|
||||
}: LoaderOptions<ModuleOptions>) {
|
||||
|
||||
console.log(
|
||||
"[HELLO MODULE] Just started the Medusa application!",
|
||||
options
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Modules Advanced Guides`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you'll learn more about developing modules and related resources.
|
||||
|
||||
By the end of this chapter, you'll know more about:
|
||||
|
||||
1. A module's container and how a module is isolated.
|
||||
2. Passing options to a module.
|
||||
3. The service factory and the methods it generates.
|
||||
4. Using a module's service to query and perform actions on the database.
|
||||
5. Using multiple services in a module.
|
||||
@@ -0,0 +1,40 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Service Constraints`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
This chapter lists constraints to keep in mind when creating a service.
|
||||
|
||||
## Use Async Methods
|
||||
|
||||
Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronus, it always executes methods in the wrapper with the `await` keyword.
|
||||
|
||||
For example, if you have a synchronous `getMessage` method, and you use it other resources like workflows, Medusa executes it as an async method:
|
||||
|
||||
```ts
|
||||
await helloModuleService.getMessage()
|
||||
```
|
||||
|
||||
So, make sure your service's methods are always async to avoid unexpected errors or behavior.
|
||||
|
||||
```ts highlights={[["8", "", "Method must be async."], ["13", "async", "Correct way of defining the method."]]}
|
||||
import { MedusaService } from "@medusajs/framework/utils"
|
||||
import MyCustom from "./models/my-custom"
|
||||
|
||||
class HelloModuleService extends MedusaService({
|
||||
MyCustom,
|
||||
}){
|
||||
// Don't
|
||||
getMessage(): string {
|
||||
return "Hello, World!"
|
||||
}
|
||||
|
||||
// Do
|
||||
async getMessage(): Promise<string> {
|
||||
return "Hello, World!"
|
||||
}
|
||||
}
|
||||
|
||||
export default HelloModuleService
|
||||
```
|
||||
@@ -0,0 +1,299 @@
|
||||
import { Tabs, TabsContent, TabsContentWrapper, TabsList, TabsTriggerVertical } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Service Factory`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about what the service factory is and how to use it.
|
||||
|
||||
## What is the Service Factory?
|
||||
|
||||
Medusa provides a service factory that your module’s main service can extend.
|
||||
|
||||
The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually.
|
||||
|
||||
<Note title="Extend the service factory when" type="success">
|
||||
|
||||
Your service provides data-management functionalities of your data models.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## How to Extend the Service Factory?
|
||||
|
||||
Medusa provides the service factory as a `MedusaService` function your service extends. The function creates and returns a service class with generated data-management methods.
|
||||
|
||||
For example, create the file `src/modules/hello/service.ts` with the following content:
|
||||
|
||||
export const highlights = [
|
||||
["4", "MedusaService", "The service factory function."],
|
||||
["5", "MyCustom", "The data models to generate data-management methods for."]
|
||||
]
|
||||
|
||||
```ts title="src/modules/hello/service.ts" highlights={highlights}
|
||||
import { MedusaService } from "@medusajs/framework/utils"
|
||||
import MyCustom from "./models/my-custom"
|
||||
|
||||
class HelloModuleService extends MedusaService({
|
||||
MyCustom,
|
||||
}){
|
||||
// TODO implement custom methods
|
||||
}
|
||||
|
||||
export default HelloModuleService
|
||||
```
|
||||
|
||||
### MedusaService Parameters
|
||||
|
||||
The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for.
|
||||
|
||||
In the example above, since the `HelloModuleService` extends `MedusaService`, it has methods to manage the `MyCustom` data model, such as `createMyCustoms`.
|
||||
|
||||
### Generated Methods
|
||||
|
||||
The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database.
|
||||
|
||||
The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`.
|
||||
|
||||
For example, the following methods are generated for the service above:
|
||||
|
||||
<Note>
|
||||
|
||||
Find a complete reference of each of the methods in [this documentation](!resources!/service-factory-reference)
|
||||
|
||||
</Note>
|
||||
|
||||
<Tabs defaultValue="listMyCustoms" layoutType="vertical" className="mt-2">
|
||||
<TabsList>
|
||||
<TabsTriggerVertical value="listMyCustoms">listMyCustoms</TabsTriggerVertical>
|
||||
<TabsTriggerVertical value="listAndCountMyCustoms">listAndCount</TabsTriggerVertical>
|
||||
<TabsTriggerVertical value="retrieveMyCustom">retrieveMyCustom</TabsTriggerVertical>
|
||||
<TabsTriggerVertical value="createMyCustoms">createMyCustoms</TabsTriggerVertical>
|
||||
<TabsTriggerVertical value="updateMyCustoms">updateMyCustoms</TabsTriggerVertical>
|
||||
<TabsTriggerVertical value="deleteMyCustoms">deleteMyCustoms</TabsTriggerVertical>
|
||||
<TabsTriggerVertical value="softDeleteMyCustoms">softDeleteMyCustoms</TabsTriggerVertical>
|
||||
<TabsTriggerVertical value="restoreMyCustoms">restoreMyCustoms</TabsTriggerVertical>
|
||||
</TabsList>
|
||||
<TabsContentWrapper className="[&_h3]:!mt-0">
|
||||
<TabsContent value="listMyCustoms">
|
||||
|
||||
### listMyCustoms
|
||||
|
||||
This method retrieves an array of records based on filters and pagination configurations.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
const myCustoms = await helloModuleService
|
||||
.listMyCustoms()
|
||||
|
||||
// with filters
|
||||
const myCustoms = await helloModuleService
|
||||
.listMyCustoms({
|
||||
id: ["123"]
|
||||
})
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="listAndCountMyCustoms">
|
||||
|
||||
### listAndCountMyCustoms
|
||||
|
||||
This method retrieves a tuple of an array of records and the total count of available records based on the filters and pagination configurations provided.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
const [
|
||||
myCustoms,
|
||||
count
|
||||
] = await helloModuleService.listAndCountMyCustoms()
|
||||
|
||||
// with filters
|
||||
const [
|
||||
myCustoms,
|
||||
count
|
||||
] = await helloModuleService.listAndCountMyCustoms({
|
||||
id: ["123"]
|
||||
})
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="retrieveMyCustom">
|
||||
|
||||
### retrieveMyCustom
|
||||
|
||||
This method retrieves a record by its ID.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
const myCustom = await helloModuleService
|
||||
.retrieveMyCustom("123")
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="createMyCustoms">
|
||||
|
||||
### createMyCustoms
|
||||
|
||||
This method creates and retrieves records of the data model.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
const myCustom = await helloModuleService
|
||||
.createMyCustoms({
|
||||
name: "test"
|
||||
})
|
||||
|
||||
// create multiple
|
||||
const myCustoms = await helloModuleService
|
||||
.createMyCustoms([
|
||||
{
|
||||
name: "test"
|
||||
},
|
||||
{
|
||||
name: "test 2"
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="updateMyCustoms">
|
||||
|
||||
### updateMyCustoms
|
||||
|
||||
This method updates and retrieves records of the data model.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
const myCustom = await helloModuleService
|
||||
.updateMyCustoms({
|
||||
id: "123",
|
||||
name: "test"
|
||||
})
|
||||
|
||||
// update multiple
|
||||
const myCustoms = await helloModuleService
|
||||
.updateMyCustoms([
|
||||
{
|
||||
id: "123",
|
||||
name: "test"
|
||||
},
|
||||
{
|
||||
id: "321",
|
||||
name: "test 2"
|
||||
},
|
||||
])
|
||||
|
||||
// use filters
|
||||
const myCustoms = await helloModuleService
|
||||
.updateMyCustoms([
|
||||
{
|
||||
selector: {
|
||||
id: ["123", "321"]
|
||||
},
|
||||
data: {
|
||||
name: "test"
|
||||
}
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="deleteMyCustoms">
|
||||
|
||||
### deleteMyCustoms
|
||||
|
||||
This method deletes records by an ID or filter.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
await helloModuleService.deleteMyCustoms("123")
|
||||
|
||||
// delete multiple
|
||||
await helloModuleService.deleteMyCustoms([
|
||||
"123", "321"
|
||||
])
|
||||
|
||||
// use filters
|
||||
await helloModuleService.deleteMyCustoms({
|
||||
selector: {
|
||||
id: ["123", "321"]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="softDeleteMyCustoms">
|
||||
|
||||
### softDeleteMyCustoms
|
||||
|
||||
This method soft-deletes records using an array of IDs or an object of filters.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
await helloModuleService.softDeleteMyCustoms("123")
|
||||
|
||||
// soft-delete multiple
|
||||
await helloModuleService.softDeleteMyCustoms([
|
||||
"123", "321"
|
||||
])
|
||||
|
||||
// use filters
|
||||
await helloModuleService.softDeleteMyCustoms({
|
||||
id: ["123", "321"]
|
||||
})
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="restoreMyCustoms">
|
||||
|
||||
### restoreMyCustoms
|
||||
|
||||
This method restores soft-deleted records using an array of IDs or an object of filters.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
await helloModuleService.restoreMyCustoms([
|
||||
"123", "321"
|
||||
])
|
||||
|
||||
// use filters
|
||||
await helloModuleService.restoreMyCustoms({
|
||||
id: ["123", "321"]
|
||||
})
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
</TabsContentWrapper>
|
||||
</Tabs>
|
||||
|
||||
### Using a Constructor
|
||||
|
||||
If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`.
|
||||
|
||||
For example:
|
||||
|
||||
```ts highlights={[["8"]]}
|
||||
import { MedusaService } from "@medusajs/framework/utils"
|
||||
import MyCustom from "./models/my-custom"
|
||||
|
||||
class HelloModuleService extends MedusaService({
|
||||
MyCustom,
|
||||
}){
|
||||
constructor() {
|
||||
super(...arguments)
|
||||
}
|
||||
}
|
||||
|
||||
export default HelloModuleService
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
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 utilize these concepts for your business use case.
|
||||
|
||||
The next chapters dive deeper into each concept, and explores Medusa's architecture. By the end of these chapters, you’ll be able to:
|
||||
|
||||
- Expose API routes with control over authentication.
|
||||
- Build sophisticated business logic in modules and manage links between them.
|
||||
- Create advanced workflows and configure retries and timeout.
|
||||
- Add new pages to the Medusa Admin.
|
||||
- Do more with subscribers, scheduled jobs, and other tools.
|
||||
@@ -0,0 +1,40 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Scheduled Jobs Number of Executions`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed.
|
||||
|
||||
## numberOfExecutions Option
|
||||
|
||||
The export configuration object of the scheduled job accepts an optional property `numberOfExecutions`. Its value is a number indicating how many times the scheduled job can be executed during the Medusa application's runtime.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["9", "numberOfExecutions", "The number of times the job should be executed."]
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
export default async function myCustomJob() {
|
||||
console.log("I'll be executed three times only.")
|
||||
}
|
||||
|
||||
export const config = {
|
||||
name: "hello-world",
|
||||
// execute every minute
|
||||
schedule: "* * * * *",
|
||||
numberOfExecutions: 3,
|
||||
}
|
||||
```
|
||||
|
||||
The above scheduled job has the `numberOfExecutions` configuration set to `3`.
|
||||
|
||||
So, it'll only execute 3 times, each every minute, then it won't be executed anymore.
|
||||
|
||||
<Note>
|
||||
|
||||
If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified.
|
||||
|
||||
</Note>
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Access Workflow Errors`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to access errors that occur during a workflow’s execution.
|
||||
|
||||
## How to Access Workflow Errors?
|
||||
|
||||
By default, when an error occurs in a workflow, it throws that error, and the execution stops.
|
||||
|
||||
You can configure the workflow to return the errors instead so that you can access and handle them differently.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["11", "errors", "`errors` is an array of errors that occur during the workflow's execution."],
|
||||
["14", "throwOnError", "Specify that errors occuring during the workflow's execution should be returned, not thrown."],
|
||||
]
|
||||
|
||||
```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import myWorkflow from "../../../workflows/hello-world"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) {
|
||||
const { result, errors } = await myWorkflow(req.scope)
|
||||
.run({
|
||||
// ...
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
if (errors.length) {
|
||||
return res.send({
|
||||
errors: errors.map((error) => error.error),
|
||||
})
|
||||
}
|
||||
|
||||
res.send(result)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The object passed to the `run` method accepts a `throwOnError` property. When disabled, the errors are returned in the `errors` property of `run`'s output.
|
||||
|
||||
The value of `errors` is an array of error objects. Each object has an `error` property, whose value is the name or text of the thrown error.
|
||||
@@ -0,0 +1,90 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Expose a Workflow Hook`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to expose a hook in your workflow.
|
||||
|
||||
## When to Expose a Hook
|
||||
|
||||
<Note title="Expose workflow hooks when" type="success">
|
||||
|
||||
Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Don't expose workflow hooks if" type="error">
|
||||
|
||||
Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## How to Expose a Hook in a Workflow?
|
||||
|
||||
To expose a hook in your workflow, use the `createHook` function imported from `@medusajs/framework/workflows-sdk`.
|
||||
|
||||
For example:
|
||||
|
||||
export const hookHighlights = [
|
||||
["13", "createHook", "Add a hook to the workflow."],
|
||||
["14", `"productCreated"`, "The hook's name."],
|
||||
["15", "productId", "The data to pass to the hook handler."],
|
||||
["19", "hooks", "Return the list of hooks in the workflow."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/my-workflow/index.ts" highlights={hookHighlights}
|
||||
import {
|
||||
createStep,
|
||||
createHook,
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { createProductStep } from "./steps/create-product"
|
||||
|
||||
export const myWorkflow = createWorkflow(
|
||||
"my-workflow",
|
||||
function (input) {
|
||||
const product = createProductStep(input)
|
||||
const productCreatedHook = createHook(
|
||||
"productCreated",
|
||||
{ productId: product.id }
|
||||
)
|
||||
|
||||
return new WorkflowResponse(product, {
|
||||
hooks: [productCreatedHook],
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `createHook` function accepts two parameters:
|
||||
|
||||
1. The first is a string indicating the hook's name. You use this to consume the hook later.
|
||||
2. The second is the input to pass to the hook handler.
|
||||
|
||||
The workflow must also pass an object having a `hooks` property as a second parameter to the `WorkflowResponse` constructor. Its value is an array of the workflow's hooks.
|
||||
|
||||
### How to Consume the Hook?
|
||||
|
||||
To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content:
|
||||
|
||||
export const handlerHighlights = [
|
||||
["3", "productCreated", "Invoke the hook, passing it a step function as a parameter."],
|
||||
]
|
||||
|
||||
```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights}
|
||||
import { myWorkflow } from "../my-workflow"
|
||||
|
||||
myWorkflow.hooks.productCreated(
|
||||
async ({ productId }, { container }) => {
|
||||
// TODO perform an action
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The hook is available on the workflow's `hooks` property using its name `productCreated`.
|
||||
|
||||
You invoke the hook, passing a step function (the hook handler) as a parameter.
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Compensation Function`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn what a compensation function is and how to add it to a step.
|
||||
|
||||
## What is a Compensation Function
|
||||
|
||||
A compensation function rolls back or undoes changes made by a step when an error occurs in the workflow.
|
||||
|
||||
For example, if a step creates a record, the compensation function deletes the record when an error occurs later in the workflow.
|
||||
|
||||
By using compensation functions, you provide a mechanism that guarantees data consistency in your application and across systems.
|
||||
|
||||
---
|
||||
|
||||
## How to add a Compensation Function?
|
||||
|
||||
A compensation function is passed as a second parameter to the `createStep` function.
|
||||
|
||||
For example, create the file `src/workflows/hello-world.ts` with the following content:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" highlights={[["15"], ["16"], ["17"]]} collapsibleLines="1-5" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
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...")
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow.
|
||||
|
||||
---
|
||||
|
||||
## Test the Compensation Function
|
||||
|
||||
Create a step in the same `src/workflows/hello-world.ts` file that throws an error:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts"
|
||||
const step2 = createStep(
|
||||
"step-2",
|
||||
async () => {
|
||||
throw new Error("Throwing an error...")
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Then, create a workflow that uses the steps:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
// other imports...
|
||||
|
||||
// steps...
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input) {
|
||||
const str1 = step1()
|
||||
step2()
|
||||
|
||||
return new WorkflowResponse({
|
||||
message: str1,
|
||||
})
|
||||
})
|
||||
|
||||
export default myWorkflow
|
||||
```
|
||||
|
||||
Finally, execute the workflow from an API route:
|
||||
|
||||
```ts title="src/api/workflow/route.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import myWorkflow from "../../../workflows/hello-world"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) {
|
||||
const { result } = await myWorkflow(req.scope)
|
||||
.run()
|
||||
|
||||
res.send(result)
|
||||
}
|
||||
```
|
||||
|
||||
Run the Medusa application and send a `GET` request to `/workflow`:
|
||||
|
||||
```bash
|
||||
curl http://localhost:9000/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.
|
||||
|
||||
---
|
||||
|
||||
## Pass Input to Compensation Function
|
||||
|
||||
If a step creates a record, the compensation function must receive the ID of the record to remove it.
|
||||
|
||||
To pass input to the compensation function, pass a second parameter in the `StepResponse` returned by the step.
|
||||
|
||||
For example:
|
||||
|
||||
export const inputHighlights = [
|
||||
["11", "", "The data to pass as an input to the compensation function."],
|
||||
["14", "{ message }", "The data received as an input from `StepResponse`'s second parameter."]
|
||||
]
|
||||
|
||||
```ts highlights={inputHighlights}
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep(
|
||||
"step-1",
|
||||
async () => {
|
||||
return new StepResponse(
|
||||
`Hello from step one!`,
|
||||
{ message: "Oops! Rolling back my changes..." }
|
||||
)
|
||||
},
|
||||
async ({ message }) => {
|
||||
console.log(message)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this example, the step passes an object as a second parameter to `StepResponse`.
|
||||
|
||||
The compensation function receives the object and uses its `message` property to log a message.
|
||||
|
||||
---
|
||||
|
||||
## Resolve Resources from the Medusa Container
|
||||
|
||||
The compensation function receives an object second parameter. The object has a `container` property that you use to resolve resources from the Medusa container.
|
||||
|
||||
For example:
|
||||
|
||||
export const containerHighlights = [
|
||||
["15", "container", "Access the container in the second parameter object."],
|
||||
["16", "resolve", "Use the container to resolve resources."]
|
||||
]
|
||||
|
||||
```ts
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||
|
||||
const step1 = createStep(
|
||||
"step-1",
|
||||
async () => {
|
||||
return new StepResponse(
|
||||
`Hello from step one!`,
|
||||
{ message: "Oops! Rolling back my changes..." }
|
||||
)
|
||||
},
|
||||
async ({ message }, { container }) => {
|
||||
const logger = container.resolve(
|
||||
ContainerRegistrationKeys.LOGGER
|
||||
)
|
||||
|
||||
logger.info(message)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this example, you use the `container` property in the second object parameter of the compensation function to resolve the logger.
|
||||
|
||||
You then use the logger to log a message.
|
||||
@@ -0,0 +1,82 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Conditions in Workflows with When-Then`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to execute an action based on a condition in a workflow using the when-then utility.
|
||||
|
||||
## Why If-Conditions Aren't Allowed in Workflows?
|
||||
|
||||
Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps.
|
||||
|
||||
At that point, variables in the workflow don't have any values. They only do when you execute the workflow.
|
||||
|
||||
So, you can't use an if-condition that checks a variable's value, as the condition will be evaluated when Medusa creates the internal representation of the workflow, rather than during execution.
|
||||
|
||||
Instead, use the when-then utility.
|
||||
|
||||
---
|
||||
|
||||
## What is the When-Then Utility?
|
||||
|
||||
The when-then utility functions execute an action if a condition is satisfied.
|
||||
|
||||
The `when` function accepts as a parameter a function that returns a boolean value, and the `then` function is chained to `when`. `then` accepts as a parameter a function that's executed if `when`'s parameter function returns a `true` value.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["15", "input", "The data to pass as a parameter to the function in the second parameter"],
|
||||
["17", "return", "The function must return a boolean value indicating whether\nthe callback function passed to `then` should be executed."],
|
||||
["19", "() => {", "The function to execute if `when`'s second parameter returns a `true` value."]
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
when,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
// step imports...
|
||||
|
||||
const workflow = createWorkflow(
|
||||
"workflow",
|
||||
function (input: {
|
||||
is_active: boolean
|
||||
}) {
|
||||
|
||||
const result = when(
|
||||
input,
|
||||
(input) => {
|
||||
return input.is_active
|
||||
}
|
||||
).then(() => {
|
||||
const stepResult = isActiveStep()
|
||||
return stepResult
|
||||
})
|
||||
|
||||
// executed without condition
|
||||
const anotherStepResult = anotherStep(result)
|
||||
|
||||
return new WorkflowResponse(
|
||||
anotherStepResult
|
||||
)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`.
|
||||
|
||||
### When Parameters
|
||||
|
||||
`when` utility is a function imported from `@medusajs/framework/workflows-sdk`. It accepts the following parameters:
|
||||
|
||||
1. The first parameter is either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter.
|
||||
2. The second parameter is a function that returns a boolean indicating whether to execute the action in `then`.
|
||||
|
||||
### Then Parameters
|
||||
|
||||
To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function.
|
||||
|
||||
The callback function is only executed if `when`'s second parameter function returns a `true` value.
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Workflow Constraints`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
This chapter lists constraints of defining a workflow or its steps.
|
||||
|
||||
## Workflow Constraints
|
||||
|
||||
### No Async Functions
|
||||
|
||||
The function passed to `createWorkflow` can’t be an async function:
|
||||
|
||||
```ts highlights={[["4", "async", "Function can't be async."], ["11", "", "Correct way of defining the function."]]}
|
||||
// Don't
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
async function (input: WorkflowInput) {
|
||||
// ...
|
||||
})
|
||||
|
||||
// Do
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
### No Direct Variable Manipulation
|
||||
|
||||
You can’t directly manipulate variables within the workflow's constructor function.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about why you can't manipulate variables [in this chapter](../conditions/page.mdx#why-if-conditions-arent-allowed-in-workflows)
|
||||
|
||||
</Note>
|
||||
|
||||
Instead, use the `transform` utility function imported from `@medusajs/framework/workflows-sdk`:
|
||||
|
||||
export const highlights = [
|
||||
["9", "", "Don't manipulate variables directly."],
|
||||
["20", "transform", "Use the `transform` function to manipulate variables."]
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
// Don't
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
const str1 = step1(input)
|
||||
const str2 = step2(input)
|
||||
|
||||
return new WorkflowResponse({
|
||||
message: `${str1}${str2}`,
|
||||
})
|
||||
})
|
||||
|
||||
// Do
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
const str1 = step1(input)
|
||||
const str2 = step2(input)
|
||||
|
||||
const result = transform(
|
||||
{
|
||||
str1,
|
||||
str2,
|
||||
},
|
||||
(input) => ({
|
||||
message: `${input.str1}${input.str2}`,
|
||||
})
|
||||
)
|
||||
|
||||
return new WorkflowResponse(result)
|
||||
})
|
||||
```
|
||||
|
||||
### No If Conditions
|
||||
|
||||
You can't use if-conditions in a workflow.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about why you can't use if-conditions [in this chapter](../conditions/page.mdx#why-if-conditions-arent-allowed-in-workflows)
|
||||
|
||||
</Note>
|
||||
|
||||
Instead, use the when-then utility function imported from `@medusajs/framework/workflows-sdk`:
|
||||
|
||||
```ts
|
||||
// Don't
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
if (input.is_active) {
|
||||
// perform an action
|
||||
}
|
||||
})
|
||||
|
||||
// Do (explained in the next chapter)
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
when(input, (input) => {
|
||||
return input.is_active
|
||||
})
|
||||
.then(() => {
|
||||
// perform an action
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### No Conditional Operators
|
||||
|
||||
You can't use conditional operators in a workflow, such as `??` or `||`.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about why you can't use if-conditions [in this chapter](../conditions/page.mdx#why-if-conditions-arent-allowed-in-workflows)
|
||||
|
||||
</Note>
|
||||
|
||||
Instead, use `transform` to store the desired value in a variable.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
// Don't
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
const message = input.message || "Hello"
|
||||
})
|
||||
|
||||
// Do
|
||||
// other imports...
|
||||
import { transform } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
const message = transform(
|
||||
{
|
||||
input
|
||||
},
|
||||
(data) => data.input.message || "hello"
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step Constraints
|
||||
|
||||
### Returned Values
|
||||
|
||||
A step must only return serializable values, such as [primitive values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values) or an object.
|
||||
|
||||
Values of other types, such as Maps, aren't allowed.
|
||||
|
||||
```ts
|
||||
// Don't
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep(
|
||||
"step-1",
|
||||
(input, { container }) => {
|
||||
const myMap = new Map()
|
||||
|
||||
// ...
|
||||
|
||||
return new StepResponse({
|
||||
myMap,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Do
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep(
|
||||
"step-1",
|
||||
(input, { container }) => {
|
||||
const myObj: Record<string, unknown> = {}
|
||||
|
||||
// ...
|
||||
|
||||
return new StepResponse({
|
||||
myObj,
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Execute Another Workflow`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to execute a workflow in another.
|
||||
|
||||
## Execute in a Workflow
|
||||
|
||||
To execute a workflow in another, use the `runAsStep` method that every workflow has.
|
||||
|
||||
For example:
|
||||
|
||||
export const workflowsHighlights = [
|
||||
["11", "runAsStep", "Use the `runAsStep` method to run the workflow as a step."],
|
||||
["12", "input", "Pass the input as you did in the `run` method before."]
|
||||
]
|
||||
|
||||
```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports"
|
||||
import {
|
||||
createWorkflow,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
createProductsWorkflow,
|
||||
} from "@medusajs/medusa/core-flows"
|
||||
|
||||
const workflow = createWorkflow(
|
||||
"hello-world",
|
||||
async (input) => {
|
||||
const products = createProductsWorkflow.runAsStep({
|
||||
input: {
|
||||
products: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Instead of invoking the workflow, passing it the container, you use its `runAsStep` method and pass it an object as a parameter.
|
||||
|
||||
The object has an `input` property to pass input to the workflow.
|
||||
|
||||
---
|
||||
|
||||
## Preparing Input Data
|
||||
|
||||
If you need to perform some data manipulation to prepare the other workflow's input data, use the `transform` utility function imported from `@medusajs/framework/workflows-sdk`.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn about the transform utility in [this chapter](../variable-manipulation/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
For example:
|
||||
|
||||
export const transformHighlights = [
|
||||
["16", "transform", "Make changes to the input data before passing it to the workflow."],
|
||||
["26", "createProductsData", "Pass the data prepared with the `transform` function to the workflow."]
|
||||
]
|
||||
|
||||
```ts highlights={transformHighlights} collapsibleLines="1-12"
|
||||
import {
|
||||
createWorkflow,
|
||||
transform,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
createProductsWorkflow,
|
||||
} from "@medusajs/medusa/core-flows"
|
||||
|
||||
type WorkflowInput = {
|
||||
title: string
|
||||
}
|
||||
|
||||
const workflow = createWorkflow(
|
||||
"hello-product",
|
||||
async (input: WorkflowInput) => {
|
||||
const createProductsData = transform({
|
||||
input,
|
||||
}, (data) => [
|
||||
{
|
||||
title: `Hello ${data.input.title}`,
|
||||
},
|
||||
])
|
||||
|
||||
const products = createProductsWorkflow.runAsStep({
|
||||
input: {
|
||||
products: createProductsData,
|
||||
},
|
||||
})
|
||||
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as an input to the `createProductsWorkflow`.
|
||||
|
||||
---
|
||||
|
||||
## Run Workflow Conditionally
|
||||
|
||||
To run a workflow in another based on a condition, use the when-then utility functions imported from `@medusajs/framework/workflows-sdk`.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn about the when-then utility in [this chapter](../conditions/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
For example:
|
||||
|
||||
export const whenHighlights = [
|
||||
["20", "when", "If `should_create` passed in the input is enabled, then run the function passed to `then`."],
|
||||
["22", "createProductsWorkflow", "Workflow only runs if `when`'s condition is `true`."]
|
||||
]
|
||||
|
||||
```ts highlights={whenHighlights} collapsibleLines="1-16"
|
||||
import {
|
||||
createWorkflow,
|
||||
when,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
createProductsWorkflow,
|
||||
} from "@medusajs/medusa/core-flows"
|
||||
import {
|
||||
CreateProductWorkflowInputDTO,
|
||||
} from "@medusajs/framework/types"
|
||||
|
||||
type WorkflowInput = {
|
||||
product?: CreateProductWorkflowInputDTO
|
||||
should_create?: boolean
|
||||
}
|
||||
|
||||
const workflow = createWorkflow(
|
||||
"hello-product",
|
||||
async (input: WorkflowInput) => {
|
||||
const product = when(input, ({ should_create }) => should_create)
|
||||
.then(() => {
|
||||
return createProductsWorkflow.runAsStep({
|
||||
input: {
|
||||
products: [input.product],
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this example, you use the `when` utility to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled.
|
||||
+378
@@ -0,0 +1,378 @@
|
||||
import { TypeList } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Long-Running Workflows`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn what a long-running workflow is and how to configure it.
|
||||
|
||||
## What is a Long-Running Workflow?
|
||||
|
||||
When you execute a workflow, you wait until the workflow finishes execution to receive the output.
|
||||
|
||||
A long-running workflow is a workflow that continues its execution in the background. You don’t receive its output immediately. Instead, you subscribe to the workflow execution to listen to status changes and receive its result once the execution is finished.
|
||||
|
||||
### Why use Long-Running Workflows?
|
||||
|
||||
Long-running workflows are useful if:
|
||||
|
||||
- A task takes too long. For example, you're importing data from a CSV file.
|
||||
- The workflow's steps wait for an external action to finish before resuming execution. For example, before you import the data from the CSV file, you wait until the import is confirmed by the user.
|
||||
|
||||
---
|
||||
|
||||
## Configure Long-Running Workflows
|
||||
|
||||
A workflow is considered long-running if at least one step has its `async` configuration set to `true` and doesn't return a step response.
|
||||
|
||||
For example, consider the following workflow and steps:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" highlights={[["15"]]} collapsibleLines="1-11" expandButtonLabel="Show More"
|
||||
import {
|
||||
createStep,
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep("step-1", async () => {
|
||||
return new StepResponse({})
|
||||
})
|
||||
|
||||
const step2 = createStep(
|
||||
{
|
||||
name: "step-2",
|
||||
async: true,
|
||||
},
|
||||
async () => {
|
||||
console.log("Waiting to be successful...")
|
||||
}
|
||||
)
|
||||
|
||||
const step3 = createStep("step-3", async () => {
|
||||
return new StepResponse("Finished three steps")
|
||||
})
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function () {
|
||||
step1()
|
||||
step2()
|
||||
const message = step3()
|
||||
|
||||
return new WorkflowResponse({
|
||||
message,
|
||||
})
|
||||
})
|
||||
|
||||
export default myWorkflow
|
||||
```
|
||||
|
||||
The second step has in its configuration object `async` set to `true` and it doesn't return a step response. This indicates that this step is an asynchronous step.
|
||||
|
||||
So, when you execute the `hello-world` workflow, it continues its execution in the background once it reaches the second step.
|
||||
|
||||
---
|
||||
|
||||
## Change Step Status
|
||||
|
||||
Once the workflow's execution reaches an async step, it'll wait in the background for the step to succeed or fail before it moves to the next step.
|
||||
|
||||
To fail or succeed a step, use the Workflow Engine Module's main service that is registered in the Medusa Container under the `Modules.WORKFLOW_ENGINE` (or `workflowsModuleService`) key.
|
||||
|
||||
### Retrieve Transaction ID
|
||||
|
||||
Before changing the status of a workflow execution's async step, you must have the execution's transaction ID.
|
||||
|
||||
When you execute the workflow, the object returned has a `transaction` property, which is an object that holds the details of the workflow execution's transaction. Use its `transactionId` to later change async steps' statuses:
|
||||
|
||||
```ts
|
||||
const { transaction } = await myWorkflow(req.scope)
|
||||
.run()
|
||||
|
||||
// use transaction.transactionId later
|
||||
```
|
||||
|
||||
### Change Step Status to Successful
|
||||
|
||||
The Workflow Engine Module's main service has a `setStepSuccess` method to set a step's status to successful. If you use it on a workflow execution's async step, the workflow continues execution to the next step.
|
||||
|
||||
For example, consider the following step:
|
||||
|
||||
export const successStatusHighlights = [
|
||||
["17", "transactionId", "Receive the workflow execution's transaction ID as an input to the step."],
|
||||
["20", "resolve", "Resolve the workflow engine's main service."],
|
||||
["24", "setStepSuccess", "Change the step's status to successful."],
|
||||
["28", "stepId", "The ID of the step as passed to `createStep`'s first parameter when it was created."],
|
||||
["29", "workflowId", "The ID of the workflow as passed to `createWorkflow`'s first parameter when it was created."],
|
||||
["31", "stepResponse", "The response returned by the step, since an `async` step can't return a response in its definition."]
|
||||
]
|
||||
|
||||
```ts highlights={successStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
Modules,
|
||||
TransactionHandlerType,
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
StepResponse,
|
||||
createStep,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
type SetStepSuccessStepInput = {
|
||||
transactionId: string
|
||||
};
|
||||
|
||||
export const setStepSuccessStep = createStep(
|
||||
"set-step-success-step",
|
||||
async function (
|
||||
{ transactionId }: SetStepSuccessStepInput,
|
||||
{ container }
|
||||
) {
|
||||
const workflowEngineService = container.resolve(
|
||||
Modules.WORKFLOW_ENGINE
|
||||
)
|
||||
|
||||
await workflowEngineService.setStepSuccess({
|
||||
idempotencyKey: {
|
||||
action: TransactionHandlerType.INVOKE,
|
||||
transactionId,
|
||||
stepId: "step-2",
|
||||
workflowId: "hello-world",
|
||||
},
|
||||
stepResponse: new StepResponse("Done!"),
|
||||
options: {
|
||||
container,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this step (which you use in a workflow other than the long-running workflow), you resolve the Workflow Engine Module's main service and set `step-2` of the previous workflow as successful.
|
||||
|
||||
The `setStepSuccess` method of the workflow engine's main service accepts as a parameter an object having the following properties:
|
||||
|
||||
<TypeList
|
||||
types={[
|
||||
{
|
||||
name: "idempotencyKey",
|
||||
type: "`object`",
|
||||
description: "The details of the workflow execution.",
|
||||
optional: false,
|
||||
children: [
|
||||
{
|
||||
name: "action",
|
||||
type: "`invoke` | `compensate`",
|
||||
description: "If the step's compensation function is running, use `compensate`. Otherwise, use `invoke`.",
|
||||
optional: false
|
||||
},
|
||||
{
|
||||
name: "transactionId",
|
||||
type: "`string`",
|
||||
description: "The ID of the workflow execution's transaction.",
|
||||
optional: false
|
||||
},
|
||||
{
|
||||
name: "stepId",
|
||||
type: "`string`",
|
||||
description: "The ID of the step to change its status. This is the first parameter passed to `createStep` when creating the step.",
|
||||
optional: false
|
||||
},
|
||||
{
|
||||
name: "workflowId",
|
||||
type: "`string`",
|
||||
description: "The ID of the workflow. This is the first parameter passed to `createWorkflow` when creating the workflow.",
|
||||
optional: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "stepResponse",
|
||||
type: "`StepResponse`",
|
||||
description: "Set the response of the step. This is similar to the response you return in a step's definition, but since the `async` step doesn't have a response, you set its response when changing its status.",
|
||||
optional: false
|
||||
},
|
||||
{
|
||||
name: "options",
|
||||
type: "`Record<string, any>`",
|
||||
description: "Options to pass to the step.",
|
||||
optional: true,
|
||||
children: [
|
||||
{
|
||||
name: "container",
|
||||
type: "`MedusaContainer`",
|
||||
description: "An instance of the Medusa Container",
|
||||
optional: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
### Change Step Status to Failed
|
||||
|
||||
The Workflow Engine Module's main service also has a `setStepFailure` method that changes a step's status to failed. It accepts the same parameter as `setStepSuccess`.
|
||||
|
||||
After changing the async step's status to failed, the workflow execution fails and the compensation functions of previous steps are executed.
|
||||
|
||||
For example:
|
||||
|
||||
export const failureStatusHighlights = [
|
||||
["17", "transactionId", "Receive the workflow execution's transaction ID as an input to the step."],
|
||||
["20", "resolve", "Resolve the workflow engine's main service."],
|
||||
["24", "setStepSuccess", "Change the step's status to successful."],
|
||||
["28", "stepId", "The ID of the step as passed to `createStep`'s first parameter when it was created."],
|
||||
["29", "workflowId", "The ID of the workflow as passed to `createWorkflow`'s first parameter when it was created."],
|
||||
["31", "stepResponse", "The response returned by the step, since an `async` step can't return a response in its definition."]
|
||||
]
|
||||
|
||||
```ts highlights={failureStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
Modules,
|
||||
TransactionHandlerType,
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
StepResponse,
|
||||
createStep,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
type SetStepFailureStepInput = {
|
||||
transactionId: string
|
||||
};
|
||||
|
||||
export const setStepFailureStep = createStep(
|
||||
"set-step-success-step",
|
||||
async function (
|
||||
{ transactionId }: SetStepFailureStepInput,
|
||||
{ container }
|
||||
) {
|
||||
const workflowEngineService = container.resolve(
|
||||
Modules.WORKFLOW_ENGINE
|
||||
)
|
||||
|
||||
await workflowEngineService.setStepFailure({
|
||||
idempotencyKey: {
|
||||
action: TransactionHandlerType.INVOKE,
|
||||
transactionId,
|
||||
stepId: "step-2",
|
||||
workflowId: "hello-world",
|
||||
},
|
||||
stepResponse: new StepResponse("Failed!"),
|
||||
options: {
|
||||
container,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
You use this step in another workflow that changes the status of an async step in a long-running workflow's execution to failed.
|
||||
|
||||
---
|
||||
|
||||
## Access Long-Running Workflow Status and Result
|
||||
|
||||
To access the status and result of a long-running workflow execution, use the `subscribe` and `unsubscribe` methods of the Workflow Engine Module's main service.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["18", "resolve", "Resolve the workflow engine from the Medusa container."],
|
||||
["30", "subscribe", "Subscribe to status changes of the workflow execution."],
|
||||
]
|
||||
|
||||
```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-11" expandButtonLabel="Show Imports"
|
||||
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import myWorkflow from "../../../workflows/hello-world"
|
||||
import {
|
||||
IWorkflowEngineService,
|
||||
} from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
||||
const { transaction, result } = await myWorkflow(req.scope).run()
|
||||
|
||||
const workflowEngineService = req.scope.resolve<
|
||||
IWorkflowEngineService
|
||||
>(
|
||||
Modules.WORKFLOW_ENGINE
|
||||
)
|
||||
|
||||
const subscriptionOptions = {
|
||||
workflowId: "hello-world",
|
||||
transactionId: transaction.transactionId,
|
||||
subscriberId: "hello-world-subscriber",
|
||||
}
|
||||
|
||||
await workflowEngineService.subscribe({
|
||||
...subscriptionOptions,
|
||||
subscriber: async (data) => {
|
||||
if (data.eventType === "onFinish") {
|
||||
console.log("Finished execution", data.result)
|
||||
// unsubscribe
|
||||
await workflowEngineService.unsubscribe({
|
||||
...subscriptionOptions,
|
||||
subscriberOrId: subscriptionOptions.subscriberId,
|
||||
})
|
||||
} else if (data.eventType === "onStepFailure") {
|
||||
console.log("Workflow failed", data.step)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
res.send(result)
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, you execute the long-running workflow `hello-world` and resolve the Workflow Engine Module's main service from the Medusa container.
|
||||
|
||||
### subscribe Method
|
||||
|
||||
The main service's `subscribe` method allows you to listen to changes in the workflow execution’s status. It accepts an object having three properties:
|
||||
|
||||
<TypeList
|
||||
types={[
|
||||
{
|
||||
name: "workflowId",
|
||||
type: "`string`",
|
||||
description: "The name of the workflow.",
|
||||
},
|
||||
{
|
||||
name: "transactionId",
|
||||
type: "`string`",
|
||||
description:
|
||||
"The ID of the workflow exection's transaction. The transaction's details are returned in the response of the workflow execution.",
|
||||
},
|
||||
{
|
||||
name: "subscriberId",
|
||||
type: "`string`",
|
||||
description:
|
||||
"The ID of the subscriber.",
|
||||
},
|
||||
{
|
||||
name: "subscriber",
|
||||
type: "`(data: { eventType: string, result?: any }) => Promise<void>`",
|
||||
description:
|
||||
"The function executed when the workflow execution's status changes. The function receives a data object. It has an `eventType` property, which you use to check the status of the workflow execution.",
|
||||
},
|
||||
]}
|
||||
sectionTitle="Access Long-Running Workflow Status and Result"
|
||||
/>
|
||||
|
||||
If the value of `eventType` in the `subscriber` function's first parameter is `onFinish`, the workflow finished executing. The first parameter then also has a `result` property holding the workflow's output.
|
||||
|
||||
### unsubscribe Method
|
||||
|
||||
You can unsubscribe from the workflow using the workflow engine's `unsubscribe` method, which requires the same object parameter as the `subscribe` method.
|
||||
|
||||
However, instead of the `subscriber` property, it requires a `subscriberOrId` property whose value is the same `subscriberId` passed to the `subscribe` method.
|
||||
|
||||
---
|
||||
|
||||
## Example: Restaurant-Delivery Recipe
|
||||
|
||||
To find a full example of a long-running workflow, refer to the [restaurant-delivery recipe](!resources!/recipes/marketplace/examples/restaurant-delivery).
|
||||
|
||||
In the recipe, you use a long-running workflow that moves an order from placed to completed. The workflow waits for the restaurant to accept the order, the driver to pick up the order, and other external actions.
|
||||
@@ -0,0 +1,14 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Workflows Advanced Development`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you'll learn about workflows in-depth and how to use them in your custom development.
|
||||
|
||||
By the end of these chapters, you'll learn about:
|
||||
|
||||
- Constructing a workflow and its constraints.
|
||||
- Using a compensation function to undo a step's action when errors occur.
|
||||
- Hooks and how to consume and expose them.
|
||||
- Configurations to retry workflows or run them in the background.
|
||||
@@ -0,0 +1,61 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Run 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, run them in parallel using the `parallelize` utility function imported from the `@medusajs/framework/workflows-sdk`.
|
||||
|
||||
The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["22", "[prices, productSalesChannel]", "The result of the steps. `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`."],
|
||||
["22", "parallelize", "Run the steps passed as parameters in parallel."],
|
||||
]
|
||||
|
||||
```ts highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
parallelize,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
createProductStep,
|
||||
getProductStep,
|
||||
createPricesStep,
|
||||
attachProductToSalesChannelStep,
|
||||
} from "./steps"
|
||||
|
||||
interface WorkflowInput {
|
||||
title: string
|
||||
}
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"my-workflow",
|
||||
(input: WorkflowInput) => {
|
||||
const product = createProductStep(input)
|
||||
|
||||
const [prices, productSalesChannel] = parallelize(
|
||||
createPricesStep(product),
|
||||
attachProductToSalesChannelStep(product)
|
||||
)
|
||||
|
||||
const id = product.id
|
||||
const refetchedProduct = getProductStep(product.id)
|
||||
|
||||
return new WorkflowResponse(refetchedProduct)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `parallelize` function accepts the steps to run in parallel as a parameter.
|
||||
|
||||
It returns an array of the steps' results in the same order they're passed to the `parallelize` function.
|
||||
|
||||
So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`.
|
||||
@@ -0,0 +1,84 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Retry Failed Steps`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to configure steps to allow retrial on failure.
|
||||
|
||||
## Configure a Step’s Retrial
|
||||
|
||||
By default, when an error occurs in a step, the step and the workflow fail, and the execution stops.
|
||||
|
||||
You can configure the step to retry on failure. The `createStep` function can accept a configuration object instead of the step’s name as a first parameter.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep(
|
||||
{
|
||||
name: "step-1",
|
||||
maxRetries: 2,
|
||||
},
|
||||
async () => {
|
||||
console.log("Executing step 1")
|
||||
|
||||
throw new Error("Oops! Something happened.")
|
||||
}
|
||||
)
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function () {
|
||||
const str1 = step1()
|
||||
|
||||
return new WorkflowResponse({
|
||||
message: str1,
|
||||
})
|
||||
})
|
||||
|
||||
export default myWorkflow
|
||||
```
|
||||
|
||||
The step’s configuration object accepts a `maxRetries` property, which is a number indicating the number of times a step can be retried when it fails.
|
||||
|
||||
When you execute the above workflow, you’ll see the following result in the terminal:
|
||||
|
||||
```bash
|
||||
Executing step 1
|
||||
Executing step 1
|
||||
Executing step 1
|
||||
error: Oops! Something happened.
|
||||
Error: Oops! Something happened.
|
||||
```
|
||||
|
||||
The first line indicates the first time the step was executed, and the next two lines indicate the times the step was retried. After that, the step and workflow fail.
|
||||
|
||||
---
|
||||
|
||||
## Step Retry Intervals
|
||||
|
||||
By default, a step is retried immediately after it fails.
|
||||
|
||||
To specify a wait time before a step is retried, pass a `retryInterval` property to the step's configuration object. Its value is a number of seconds to wait before retrying the step.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" highlights={[["5"]]}
|
||||
const step1 = createStep(
|
||||
{
|
||||
name: "step-1",
|
||||
maxRetries: 2,
|
||||
retryInterval: 2, // 2 seconds
|
||||
},
|
||||
async () => {
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Variable Manipulation in Workflows with transform`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to manipulate variables in a workflow using the transform utility.
|
||||
|
||||
## Why Variable Manipulation isn't Allowed in Worflows?
|
||||
|
||||
Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps.
|
||||
|
||||
At that point, variables in the workflow don't have any values. They only do when you execute the workflow.
|
||||
|
||||
So, you can only pass variables as parameters to steps. But, in a workflow, you can't change a variable's value or, if the variable is an array, loop over its items.
|
||||
|
||||
Instead, use the transform utility.
|
||||
|
||||
---
|
||||
|
||||
## What is the transform Utility?
|
||||
|
||||
The `transform` utility function creates a new variable as the result of manipulating other variables.
|
||||
|
||||
For example, consider you have two strings as the output of two steps:
|
||||
|
||||
```ts
|
||||
const str1 = step1()
|
||||
const str2 = step2()
|
||||
```
|
||||
|
||||
To concatinate the strings, you create a new variable `str3` using the `transform` function:
|
||||
|
||||
export const highlights = [
|
||||
["14", "str3", "Holds the result returned by `transform`'s second parameter function."],
|
||||
["15", "", "Specify the data to pass as a parameter to the function in the second parameter."],
|
||||
["16", "data", "The data passed in the first parameter of `transform`."],
|
||||
["16", "`${data.str1}${data.str2}`", "Return the concatenated strings."]
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
transform,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
// step imports...
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input) {
|
||||
const str1 = step1(input)
|
||||
const str2 = step2(input)
|
||||
|
||||
const str3 = transform(
|
||||
{ str1, str2 },
|
||||
(data) => `${data.str1}${data.str2}`
|
||||
)
|
||||
|
||||
return new WorkflowResponse(str3)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `transform` utility function is imported from `@medusajs/framework/workflows-sdk`. It accepts two parameters:
|
||||
|
||||
1. The first parameter is an object of variables to manipulate. The object is passed as a parameter to `transform`'s second parameter function.
|
||||
2. The second parameter is the function performing the variable manipulation.
|
||||
|
||||
The value returned by the second parameter function is returned by `transform`. So, the `str3` variable holds the concatenated string.
|
||||
|
||||
You can use the returned value in the rest of the workflow, either to pass it as an input to other steps or to return it in the workflow's response.
|
||||
|
||||
---
|
||||
|
||||
## Example: Looping Over Array
|
||||
|
||||
Use `transform` to loop over arrays to create another variable from the array's items.
|
||||
|
||||
For example:
|
||||
|
||||
```ts collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
transform,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
// step imports...
|
||||
|
||||
type WorkflowInput = {
|
||||
items: {
|
||||
id: string
|
||||
name: string
|
||||
}[]
|
||||
}
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function ({ items }: WorkflowInput) {
|
||||
const ids = transform(
|
||||
{ items },
|
||||
(data) => data.items.map((item) => item.id)
|
||||
)
|
||||
|
||||
doSomethingStep(ids)
|
||||
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This workflow receives an `items` array in its input.
|
||||
|
||||
You use the `transform` utility to create an `ids` variable, which is an array of strings holding the `id` of each item in the `items` array.
|
||||
|
||||
You then pass the `ids` variable as a parameter to the `doSomethingStep`.
|
||||
@@ -0,0 +1,150 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Workflow Hooks`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn what a workflow hook is and how to consume them.
|
||||
|
||||
## What is a Workflow Hook?
|
||||
|
||||
A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler.
|
||||
|
||||
Medusa exposes hooks in many of its workflows that are used in its API routes. You can consume those hooks to add your custom logic.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Refer to the [Workflows Reference](!resources!/medusa-workflows-reference) to view all workflows and their hooks.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Consume workflow hooks when" type="success">
|
||||
|
||||
You want to perform a custom action during a workflow's execution, such as when a product is created.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## How to Consume a Hook?
|
||||
|
||||
A workflow has a special `hooks` property which is an object that holds its hooks.
|
||||
|
||||
So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory:
|
||||
|
||||
- Import the workflow.
|
||||
- Access its hook using the `hooks` property.
|
||||
- Pass the hook a step function as a parameter to consume it.
|
||||
|
||||
For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content:
|
||||
|
||||
export const handlerHighlights = [
|
||||
["3", "productsCreated", "Invoke the hook, passing it a step function as a parameter."],
|
||||
]
|
||||
|
||||
```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights}
|
||||
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
|
||||
|
||||
createProductsWorkflow.hooks.productsCreated(
|
||||
async ({ products }, { container }) => {
|
||||
// TODO perform an action
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `productsCreated` hook is available on the workflow's `hooks` property by its name.
|
||||
|
||||
You invoke the hook, passing a step function (the hook handler) as a parameter.
|
||||
|
||||
Now, when a product is created using the [Create Product API route](!api!/admin#products_postproducts), your hook handler is executed after the product is created.
|
||||
|
||||
<Note>
|
||||
|
||||
A hook can have only one handler.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Refer to the [createProductsWorkflow reference](!resources!/references/medusa-workflows/createProductsWorkflow) to see at which point the hook handler is executed.
|
||||
|
||||
</Note>
|
||||
|
||||
### Hook Handler Parameter
|
||||
|
||||
Since a hook handler is essentially a step function, it receives the hook's input as a first parameter, and an object holding a `container` property as a second parameter.
|
||||
|
||||
Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product.
|
||||
|
||||
### Hook Handler Compensation
|
||||
|
||||
Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/workflows/hooks/product-created.ts"
|
||||
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
|
||||
|
||||
createProductsWorkflow.productCreated(
|
||||
async ({ productId }, { container }) => {
|
||||
// TODO perform an action
|
||||
|
||||
return new StepResponse(undefined, { ids })
|
||||
},
|
||||
async ({ ids }, { container }) => {
|
||||
// undo the performed action
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler.
|
||||
|
||||
The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function.
|
||||
|
||||
It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container.
|
||||
|
||||
### Additional Data Property
|
||||
|
||||
Medusa's workflows pass in the hook's input an `additional_data` property:
|
||||
|
||||
```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]}
|
||||
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
|
||||
|
||||
createProductsWorkflow.hooks.productsCreated(
|
||||
async ({ products, additional_data }, { container }) => {
|
||||
// TODO perform an action
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn how to pass `additional_data` in requests to API routes in [this chapter](../../api-routes/additional-data/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
### Pass Additional Data to Workflow
|
||||
|
||||
You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow:
|
||||
|
||||
```ts title="src/workflows/hooks/product-created.ts" highlights={[["10", "additional_data"]]}
|
||||
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
|
||||
|
||||
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
||||
await createProductsWorkflow(req.scope).run({
|
||||
input: {
|
||||
products: [
|
||||
// ...
|
||||
],
|
||||
additional_data: {
|
||||
custom_field: "test",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Your hook handler then receives that passed data in the `additional_data` object.
|
||||
@@ -0,0 +1,100 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Workflow Timeout`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to set a timeout for workflows and steps.
|
||||
|
||||
## What is a Workflow Timeout?
|
||||
|
||||
By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs.
|
||||
|
||||
You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown.
|
||||
|
||||
### Timeout Doesn't Stop Step Execution
|
||||
|
||||
Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result.
|
||||
|
||||
---
|
||||
|
||||
## Configure Workflow Timeout
|
||||
|
||||
The `createWorkflow` function can accept a configuration object instead of the workflow’s name.
|
||||
|
||||
In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More"
|
||||
import {
|
||||
createStep,
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep(
|
||||
"step-1",
|
||||
async () => {
|
||||
// ...
|
||||
}
|
||||
)
|
||||
|
||||
const myWorkflow = createWorkflow({
|
||||
name: "hello-world",
|
||||
timeout: 2, // 2 seconds
|
||||
}, function () {
|
||||
const str1 = step1()
|
||||
|
||||
return new WorkflowResponse({
|
||||
message: str1,
|
||||
})
|
||||
})
|
||||
|
||||
export default myWorkflow
|
||||
|
||||
```
|
||||
|
||||
This workflow's executions fail if they run longer than two seconds.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](../access-workflow-errors/page.mdx). The error’s name is `TransactionTimeoutError`.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Configure Step Timeout
|
||||
|
||||
Alternatively, you can configure the timeout for a step rather than the entire workflow.
|
||||
|
||||
<Note>
|
||||
|
||||
As mentioned in the previous section, the timeout doesn't stop the execution of the step. It only affects the step's status and output.
|
||||
|
||||
</Note>
|
||||
|
||||
The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds.
|
||||
|
||||
For example:
|
||||
|
||||
```tsx
|
||||
const step1 = createStep(
|
||||
{
|
||||
name: "step-1",
|
||||
timeout: 2, // 2 seconds
|
||||
},
|
||||
async () => {
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This step's executions fail if they run longer than two seconds.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](../access-workflow-errors/page.mdx). The error’s name is `TransactionStepTimeoutError`.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,64 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Admin Customizations`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to customize the Medusa Admin dashboard.
|
||||
|
||||
## What is the Medusa Admin?
|
||||
|
||||
The Medusa Admin is an admin dashboard that merchants use to manage their store's data.
|
||||
|
||||
You can extend the Medusa Admin to add widgets and new pages. In your customizations, you interact with API routes to provide merchants with custom functionalities.
|
||||
|
||||
The Medusa Admin is installed in your Medusa application and runs at `http://localhost:9000/app` when you start the application.
|
||||
|
||||
---
|
||||
|
||||
## Example: Create a Widget
|
||||
|
||||
A widget is a React component that can be injected into an existing page in the admin dashboard.
|
||||
|
||||
For example, create the file `src/admin/widgets/product-widget.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx"
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
|
||||
const ProductWidget = () => {
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">Product Widget</Heading>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
This inserts a widget with the text “Product Widget” at the beginning of a product’s details page.
|
||||
|
||||
In your widget, use custom components from the [Medusa UI package](https://docs.medusajs.com/ui).
|
||||
|
||||
### Test the Widget
|
||||
|
||||
To test out the widget, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, open a product’s details page in the Medusa Admin. You’ll find your custom widget at the top of the page.
|
||||
|
||||
---
|
||||
|
||||
## Admin Components List
|
||||
|
||||
To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](!resources!/admin-component) to find common components.
|
||||
@@ -0,0 +1,63 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} API Routes`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn what API Routes are and how to create them.
|
||||
|
||||
## What is an API Route?
|
||||
|
||||
An API Route is a REST API endpoint. It exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems.
|
||||
|
||||
The Medusa core application provides a set of admin and store API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities.
|
||||
|
||||
---
|
||||
|
||||
## How to Create an API Route?
|
||||
|
||||
An API Route is created in a TypeScript or JavaScript file under the `src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`.
|
||||
|
||||
Each file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…).
|
||||
|
||||
For example, to create a `GET` API Route at `/hello-world`, create the file `src/api/hello-world/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/hello-world/route.ts"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
|
||||
export const GET = (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: "[GET] Hello world!",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Test API Route
|
||||
|
||||
To test the API route above, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, send a `GET` request to the `/hello-world` API Route:
|
||||
|
||||
```bash
|
||||
curl http://localhost:9000/hello-world
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use API Routes
|
||||
|
||||
<Note title="Use API routes when" type="success">
|
||||
|
||||
You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,55 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Commerce Modules`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about Medusa's commerce modules.
|
||||
|
||||
## What is a Commerce Module?
|
||||
|
||||
Medusa provides all its commerce features as separate commerce modules, such as the Product or Order modules. Medusa uses these modules in its API routes to expose their commerce features.
|
||||
|
||||
Medusa's commerce modules and your custom modules are interchangeable in the Medusa application, making Medusa’s architecture more flexible.
|
||||
|
||||
### List of Medusa's Commerce Modules
|
||||
|
||||
Refer to [this reference](!resources!/commerce-modules) for a full list of commerce modules in Medusa.
|
||||
|
||||
---
|
||||
|
||||
## Resolve Commerce Module Services
|
||||
|
||||
Similarly to your custom module, a commerce module's main service is registered in the Medusa container. So, you can resolve it in your resources, such as API routes, to use its functionality.
|
||||
|
||||
For example, you saw this code snippet in the [Medusa container chapter](../medusa-container/page.mdx):
|
||||
|
||||
```ts highlights={[["10"]]}
|
||||
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import { IProductModuleService } from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const productModuleService: IProductModuleService = req.scope.resolve(
|
||||
Modules.PRODUCT
|
||||
)
|
||||
|
||||
const [, count] = await productModuleService
|
||||
.listAndCountProducts()
|
||||
|
||||
res.json({
|
||||
count,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
When you resolve the `Modules.PRODUCT` (or `productModuleService`) registration name, you're actually resolving the main service of the Product Module.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
To resolve the main service of any commerce module, use the registration name defined in the `Modules` enum imported from `@medusajs/framework/utils`.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,123 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Events and Subscribers`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to handle events with subscribers.
|
||||
|
||||
## What is an Event?
|
||||
|
||||
When an action is performed in Medusa, such as creating a product, the Medusa application emits an event.
|
||||
|
||||
You can listen to those events and perform an asynchronous action using a subscriber.
|
||||
|
||||
---
|
||||
|
||||
## What is a Subscriber?
|
||||
|
||||
A subscriber is a function executed whenever the event it listens to is emitted.
|
||||
|
||||
### How to Create a Subscriber?
|
||||
|
||||
A subscriber is created in a TypeScript or JavaScript file under the `src/subscribers` directory.
|
||||
|
||||
For example, create the file `src/subscribers/product-created.ts` with the following content:
|
||||
|
||||
```ts title="src/subscribers/product-created.ts"
|
||||
import { type SubscriberConfig } from "@medusajs/framework"
|
||||
|
||||
// subscriber function
|
||||
export default async function productCreateHandler() {
|
||||
console.log("A product was created")
|
||||
}
|
||||
|
||||
// subscriber config
|
||||
export const config: SubscriberConfig = {
|
||||
event: "product.created",
|
||||
}
|
||||
```
|
||||
|
||||
A subscriber file must export:
|
||||
|
||||
- A subscriber function that is an asynchronous function executed whenever the associated event is triggered.
|
||||
- A configuration object defining the event this subscriber is listening to.
|
||||
|
||||
The above subscriber listens to the `product.created` event. Whenever the event is emitted, it logs in the terminal `A product is created`.
|
||||
|
||||
---
|
||||
|
||||
## Test the Subscriber
|
||||
|
||||
To test the subscriber, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, go to the Medusa Admin at `localhost:9000/app` and create a product. You’ll see the following messages logged in the Medusa application's terminal:
|
||||
|
||||
```bash
|
||||
info: Processing product.created which has 1 subscribers
|
||||
A product was created
|
||||
```
|
||||
|
||||
The first message indicates that the `product.created` event was emitted, and the second one is the message logged from the subscriber.
|
||||
|
||||
---
|
||||
|
||||
## When to Use Subscribers
|
||||
|
||||
<Note title="Use subscribers when" type="success">
|
||||
|
||||
You want to perform an action everytime a specific event is emitted in the Medusa application.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Resolve Resources
|
||||
|
||||
The subscriber function accepts an object parameter with the property `container`. Its value is the Medusa container, and you can use it to resolve other resources, such as services.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["7", "container", "Recieve the Medusa Container in the object parameter."],
|
||||
["10", "resolve", "Resolve the Product Module's main service."],
|
||||
["10", "Modules.PRODUCT", "The module's registration name imported from `@medusajs/framework/utils`."]
|
||||
]
|
||||
|
||||
```ts title="src/subscribers/product-created.ts" highlights={highlights}
|
||||
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
|
||||
import { IProductModuleService } from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
export default async function productCreateHandler({
|
||||
event: { data },
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const productModuleService: IProductModuleService =
|
||||
container.resolve(Modules.PRODUCT)
|
||||
|
||||
const productId = data.id
|
||||
|
||||
const product = await productModuleService.retrieveProduct(
|
||||
productId
|
||||
)
|
||||
|
||||
console.log(`The product ${product.title} was created`)
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: `product.created`,
|
||||
}
|
||||
```
|
||||
|
||||
You use the container to resolve the Product Module's main service, then log the title of the created product.
|
||||
|
||||
---
|
||||
|
||||
## Events List
|
||||
|
||||
Find a list of all emitted events in [this reference](!resources!/events-reference).
|
||||
@@ -0,0 +1,78 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Loaders`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about loaders and how to use them.
|
||||
|
||||
## What is a Loader?
|
||||
|
||||
A loader is a function executed when the Medusa application starts. You define and export it in a module.
|
||||
|
||||
Loaders are useful to perform a task at the application start-up, such as to sync data between Medusa and a third-pary service.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Loader?
|
||||
|
||||
A loader is created in a TypeScript or JavaScript file under a module's `loaders` directory.
|
||||
|
||||
For example, create the file `src/modules/hello/loaders/hello-world.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/loaders/hello-world.ts"
|
||||
export default async function helloWorldLoader() {
|
||||
console.log(
|
||||
"[HELLO MODULE] Just started the Medusa application!"
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Export Loader in Module Definition
|
||||
|
||||
Import the loader in `src/modules/hello/index.ts` and export it in the module's definition:
|
||||
|
||||
```ts title="src/modules/hello/index.ts"
|
||||
// other imports...
|
||||
import helloWorldLoader from "./loaders/hello-world"
|
||||
|
||||
export default Module("hello", {
|
||||
// ...
|
||||
loaders: [helloWorldLoader],
|
||||
})
|
||||
```
|
||||
|
||||
The value of the `loaders` property is an array of loader functions.
|
||||
|
||||
---
|
||||
|
||||
## Test the Loader
|
||||
|
||||
Start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Among the messages logged in the terminal, you’ll see the following message:
|
||||
|
||||
```bash
|
||||
[HELLO MODULE] Just started the Medusa application!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use Loaders
|
||||
|
||||
<Note title="Use loaders when" type="success">
|
||||
|
||||
- You're performing an action at application start-up.
|
||||
- You're establishing a one-time connection with an external system.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Don't use loaders if" type="error">
|
||||
|
||||
You want to perform an action continuously or at a set time pattern in the application. Use scheduled jobs instead, which is explained in an upcoming chapter.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,46 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Medusa container`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about the Medusa container and how to use it.
|
||||
|
||||
## What is the Medusa container?
|
||||
|
||||
The Medusa container holds all resources registered in the Medusa application, such as services.
|
||||
|
||||
In your customizations, you use the Medusa container to resolve these resources and use their functionalities.
|
||||
|
||||
For example, in a custom API route you can resolve any service registered in the Medusa application using the `scope.resolve` method of the `MedusaRequest` parameter:
|
||||
|
||||
export const highlights = [
|
||||
["9", "resolve", "Resolve the Product Module's main service."],
|
||||
[
|
||||
"10",
|
||||
"Modules.PRODUCT",
|
||||
"The resource registration name imported from `@medusajs/framework/utils`.",
|
||||
],
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import { IProductModuleService } from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const productModuleService: IProductModuleService = req.scope.resolve(
|
||||
Modules.PRODUCT
|
||||
)
|
||||
|
||||
const [, count] = await productModuleService
|
||||
.listAndCountProducts()
|
||||
|
||||
res.json({
|
||||
count,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Modules Directory Structure`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this document, you'll learn about the expected files and directories in your module.
|
||||
|
||||

|
||||
|
||||
## index.ts
|
||||
|
||||
The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](../modules/page.mdx).
|
||||
|
||||
---
|
||||
|
||||
## service.ts
|
||||
|
||||
A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](../modules/page.mdx).
|
||||
|
||||
---
|
||||
|
||||
## Other Directories
|
||||
|
||||
The following directories are optional and their content are explained more in the following chapters:
|
||||
|
||||
- `models`: Holds the data models representing tables in the database.
|
||||
- `migrations`: Holds the migration files used to reflect changes on the database.
|
||||
- `loaders`: Holds the scripts to run on the Medusa application's start-up.
|
||||
@@ -0,0 +1,266 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Modules`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about modules and how to create them.
|
||||
|
||||
## What is a Module?
|
||||
|
||||
A module is a package of reusable commerce or architectural functionalities.
|
||||
|
||||
In Medusa, modules handle business logic in a class called a service, and define and manage data models that represent tables in the database.
|
||||
|
||||
Out of the box, Medusa comes with multiple pre-built modules for core commerce needs. For example, the Cart Module holds the data models and business logic for cart operations.
|
||||
|
||||
As you learn more about Medusa, you will see that Modules are central to customizations and integrations.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Module?
|
||||
|
||||
In this section, you'll build a module that has a `MyCustom` data model and a service to manage that data model. You'll then use the module's service in an API route to create a record of `MyCustom`.
|
||||
|
||||
Modules are created in a sub-directory of `src/modules`.
|
||||
|
||||
For example, create the directory `src/modules/hello`.
|
||||
|
||||
### 1. Create Data Model
|
||||
|
||||
A data model represents a table in the database. It's created in a TypeScript or JavaScript file under the module's `models` directory.
|
||||
|
||||
For example, create the file `src/modules/hello/models/my-custom.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/models/my-custom.ts"
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text(),
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
You define the data model using the `define` method of the `model` utility imported from `@medusajs/framework/utils`. It accepts two parameters:
|
||||
|
||||
1. The first one is the name of the data model's table in the database. It should be snake-case.
|
||||
2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods.
|
||||
|
||||
The example above defines the data model `MyCustom` with the properties `id` and `name`.
|
||||
|
||||
<Note>
|
||||
|
||||
Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`.
|
||||
|
||||
</Note>
|
||||
|
||||
### 2. Create Service
|
||||
|
||||
A module must define a service that implements its functionalities, such as manage the records of your custom data models in the database.
|
||||
|
||||
A service is a TypeScript or JavaScript class defined in the `service.ts` file at the root of your module's directory.
|
||||
|
||||
For example, create the file `src/modules/hello/service.ts` with the following content:
|
||||
|
||||
export const highlights = [
|
||||
["4", "MedusaService", "The service factory function."],
|
||||
["5", "MyCustom", "The data models to generate data-management methods for."]
|
||||
]
|
||||
|
||||
```ts title="src/modules/hello/service.ts" highlights={highlights}
|
||||
import { MedusaService } from "@medusajs/framework/utils"
|
||||
import MyCustom from "./models/my-custom"
|
||||
|
||||
class HelloModuleService extends MedusaService({
|
||||
MyCustom,
|
||||
}){
|
||||
}
|
||||
|
||||
export default HelloModuleService
|
||||
```
|
||||
|
||||
In the snippet above, your module's service extends a class generated by the `MedusaService` utility function, which is the service factory.
|
||||
|
||||
The `MedusaService` function accepts as a parameter an object of data models, and returns a class with generated methods for data-management Create, Read, Update, and Delete (CRUD) operations on those data models.
|
||||
|
||||
For example, `HelloModuleService` now has a `createMyCustoms` method to create `MyCustom` records, and `retrieveMyCustom` to retrive a `MyCustom` record.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
If a module doesn't have data models, it doesn't need to extend `MedusaService`.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
|
||||
You'll learn more about the methods generated by the service factory in later chapters.
|
||||
|
||||
</Note>
|
||||
|
||||
### 3. Export Module Definition
|
||||
|
||||
A module must have an `index.ts` file in its root directory. The file exports the module's definition.
|
||||
|
||||
For example, create the file `src/modules/hello/index.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/index.ts" highlights={[["7", "", "The main service of the module."]]}
|
||||
import HelloModuleService from "./service"
|
||||
import { Module } from "@medusajs/framework/utils"
|
||||
|
||||
export const HELLO_MODULE = "helloModuleService"
|
||||
|
||||
export default Module(HELLO_MODULE, {
|
||||
service: HelloModuleService,
|
||||
})
|
||||
```
|
||||
|
||||
You use the `Module` function imported from `@medusajs/framework/utils` to create the module's definition. It accepts two parameters:
|
||||
|
||||
1. The name that the module's main service is registered under (`helloModuleService`).
|
||||
2. An object with a required property `service` indicating the module's main service.
|
||||
|
||||
### 4. Add Module to Configurations
|
||||
|
||||
The last step is to add the module in Medusa’s configurations.
|
||||
|
||||
In `medusa-config.ts`, add a `modules` property and pass in it your custom module:
|
||||
|
||||
```ts title="medusa-config.ts" highlights={[["7"]]}
|
||||
module.exports = defineConfig({
|
||||
projectConfig: {
|
||||
// ...
|
||||
},
|
||||
modules: [
|
||||
{
|
||||
resolve: "./src/modules/hello",
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
Its value is an array of objects, each having a `resolve` property, whose value is either a path to module's directory, or an `npm` package’s name.
|
||||
|
||||
### 5. Generate Migrations
|
||||
|
||||
A migration is a TypeScript or JavaScript file that defines database changes made by your module, such as create the `my_custom` table for the `MyCustom` data model.
|
||||
|
||||
To generate a migration for the data models in your module, run the following command:
|
||||
|
||||
```bash
|
||||
npx medusa db:generate helloModuleService
|
||||
```
|
||||
|
||||
The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for.
|
||||
|
||||
<Note>
|
||||
|
||||
The module name `helloModuleService` is the key used when registering the module in Medusa's `modules` configuration.
|
||||
|
||||
</Note>
|
||||
|
||||
The above command creates a migration file at the directory `src/modules/hello/migrations` similar to the following:
|
||||
|
||||
```ts
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20240702105919 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql("create table if not exists \"my_custom\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"my_custom_pkey\" primary key (\"id\"));")
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql("drop table if exists \"my_custom\" cascade;")
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
In the migration class, the `up` method creates the table `my_custom` and defines its columns using PostgreSQL syntax. The `down` method drops the table.
|
||||
|
||||
### 6. Run Migrations
|
||||
|
||||
To reflect the changes in the generated migration file, run the `db:migrate` command:
|
||||
|
||||
```bash
|
||||
npx medusa db:migrate
|
||||
```
|
||||
|
||||
This creates the `my_custom` table in the database.
|
||||
|
||||
---
|
||||
|
||||
## Test the Module
|
||||
|
||||
Since the module's main service is registered in the Medusa container, you can resolve it in other resources to use its methods.
|
||||
|
||||
For example, create the API route `src/api/custom/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/custom/route.ts"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import HelloModuleService from "../../modules/hello/service"
|
||||
import { HELLO_MODULE } from "../../modules/hello"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService = req.scope.resolve(
|
||||
HELLO_MODULE
|
||||
)
|
||||
|
||||
const my_custom = await helloModuleService.createMyCustoms({
|
||||
name: "test"
|
||||
})
|
||||
|
||||
res.json({
|
||||
my_custom
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
You resolve the Hello Module's main service and use its generated method `createMyCustoms` to create a new record in the database, then return that record.
|
||||
|
||||
Then, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Finally, send a `GET` request to `/custom`:
|
||||
|
||||
```bash
|
||||
curl http://localhost:9000/custom
|
||||
```
|
||||
|
||||
You’ll receive the following response:
|
||||
|
||||
```json
|
||||
{
|
||||
"my_custom": {
|
||||
"id": "123...",
|
||||
"name": "test",
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why Use Modules
|
||||
|
||||
In digital commerce, you often need to introduce custom behavior specific to your products, industry, tech stack, or your general ways of working. In other commerce platforms, introducing custom business logic and data models requires setting up separate applications to manage these customizations.
|
||||
|
||||
Medusa removes this overhead by allowing you to easily write custom Modules that integrate into the Medusa application without implications on the existing setup.
|
||||
|
||||
<Note title="Use modules when" type="success">
|
||||
|
||||
- You're adding a new table to the database.
|
||||
- You're extending an existing table in the database to add custom fields, which is explained in later chapters.
|
||||
- You're integrating a third-party system for commerce or architectural features, as explained in later chapters.
|
||||
- You want to re-use your custom commerce functionalities across Medusa applications or use them in other environments, such as Edge functions and Next.js apps.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,18 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} The Basics`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you’ll learn about the basic concepts of Medusa that are central in your development.
|
||||
|
||||
By the end of these chapter, you’ll be able to:
|
||||
|
||||
- Expose your custom functionalities through endpoints.
|
||||
- Create custom modules that define custom business logic.
|
||||
- Create custom tables in the database through data models.
|
||||
- Execute scripts when the Medusa application starts.
|
||||
- Perform asynchronus actions when an event occurs.
|
||||
- Run tasks at a specified time or pattern during the Medusa application's runtime.
|
||||
- Create custom flows as a series of steps involving multiple services.
|
||||
- Customize the admin dashboard to inject components on existing pages or add new pages.
|
||||
@@ -0,0 +1,23 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Project File Conventions`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about important directories and files in your Medusa application's project.
|
||||
|
||||
## src
|
||||
|
||||
This directory is the central place for your custom development. It includes the following sub-directories:
|
||||
|
||||
- `admin`: Holds your admin dashboard's custom components and pages.
|
||||
- `api`: Holds your custom API routes that are added as endpoints in your Medusa application.
|
||||
- `jobs`: Holds your scheduled jobs that run at a specified interval during your Medusa application's runtime.
|
||||
- `modules`: Holds your custom modules that implement custom business logic.
|
||||
- `scripts`: Holds your custom scripts to be executed using Medusa's CLI tool.
|
||||
- `subscribers`: Holds your event listeners that are executed asynchronously whenever an event is emitted.
|
||||
- `workflows`: Holds your custom flows that can be executed from anywhere in your application.
|
||||
|
||||
## medusa-config.ts
|
||||
|
||||
This file holds your Medusa configurations, such as your PostgreSQL database configurations.
|
||||
@@ -0,0 +1,120 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Scheduled Jobs`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about scheduled jobs and how to use them.
|
||||
|
||||
## What is a Scheduled Job?
|
||||
|
||||
A scheduled job is a function executed at a specified interval of time in the background of your Medusa application. It’s like a cron job that runs during the application's runtime.
|
||||
|
||||
For example, you can synchronize your inventory with an Enterprise Resource Planning (ERP) system once a day using a scheduled job.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Scheduled Job?
|
||||
|
||||
A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory.
|
||||
|
||||
For example, create the file `src/jobs/hello-world.ts` with the following content:
|
||||
|
||||
```ts title="src/jobs/hello-world.ts"
|
||||
// the scheduled-job function
|
||||
export default function () {
|
||||
console.log("Time to say hello world!")
|
||||
}
|
||||
|
||||
// the job's configurations
|
||||
export const config = {
|
||||
name: "every-minute-message",
|
||||
// execute every minute
|
||||
schedule: "* * * * *",
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
A scheduled job file must export:
|
||||
|
||||
- A function to be executed whenever it’s time to run the scheduled job.
|
||||
- A configuration object defining the job. It has two properties:
|
||||
- `name`: a unique name for the job.
|
||||
- `schedule`: a [cron expression](https://crontab.guru/) specifying when to run the job.
|
||||
|
||||
This scheduled job executes every minute and logs into the terminal `Time to say hello world!`.
|
||||
|
||||
### Test Scheduled Jobs
|
||||
|
||||
To test out your scheduled job, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
After a minute, the following message will be logged to the terminal:
|
||||
|
||||
```bash
|
||||
Time to say hello world!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use Scheduled Jobs
|
||||
|
||||
<Note title="Use scheduled jobs when" type="success">
|
||||
|
||||
- You're executing an action at a specified time interval during application runtime.
|
||||
- The action must be executed automatically.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Don't use scheduled jobs if" type="error">
|
||||
|
||||
- You want the action to execute at a specified time interval while the Medusa application **isn't** running. Instead, use the operating system's equivalent of a cron job.
|
||||
- You want to execute the action once. Use loaders instead.
|
||||
- You want to execute the action if an event occurs. Use subscribers instead.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Resolve Resources
|
||||
|
||||
The scheduled job function receives a `container` parameter, which is the Medusa container. Use it to resolve resources in your Medusa application, such as services.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["11", "resolve", "Resolve the Product Module's main service."],
|
||||
["11", "Modules.PRODUCT", "The module's registration name imported from `@medusajs/framework/utils`."]
|
||||
]
|
||||
|
||||
```ts title="src/jobs/hello-world.ts" highlights={highlights}
|
||||
import {
|
||||
IProductModuleService,
|
||||
MedusaContainer,
|
||||
} from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
export default async function myCustomJob(
|
||||
container: MedusaContainer
|
||||
) {
|
||||
const productModuleService: IProductModuleService =
|
||||
container.resolve(Modules.PRODUCT)
|
||||
|
||||
const [, count] = await productModuleService.listAndCountProducts()
|
||||
|
||||
console.log(
|
||||
`Time to check products! You have ${count} product(s)`
|
||||
)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
name: "every-minute-message",
|
||||
// execute every minute
|
||||
schedule: "* * * * *",
|
||||
}
|
||||
```
|
||||
|
||||
In the scheduled job function, you resolve the Product Module's main service and retrieve the number of products in the store, then log the number in the terminal.
|
||||
@@ -0,0 +1,270 @@
|
||||
import { CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Workflows`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about workflows and how to define and execute them.
|
||||
|
||||
## What is a Workflow?
|
||||
|
||||
A workflow is a series of queries and actions that complete a task.
|
||||
|
||||
You construct a workflow similar to how you create a JavaScript function, but unlike regular functions, a workflow creates an internal representation of your steps.
|
||||
|
||||
By using a workflow, you can track its execution's progress, provide roll-back logic for each step to mitigate data inconsistency when errors occur, automatically retry failing steps, and do much more, as explained in later chapters.
|
||||
|
||||
---
|
||||
|
||||
## How to Create and Execute a Workflow?
|
||||
|
||||
### 1. Create the Steps
|
||||
|
||||
A workflow is made of a series of steps. A step is created using the `createStep` utility function imported from `@medusajs/framework/workflows-sdk`.
|
||||
|
||||
Create the file `src/workflows/hello-world.ts` with the following content:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts"
|
||||
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep("step-1", async () => {
|
||||
return new StepResponse(`Hello from step one!`)
|
||||
})
|
||||
```
|
||||
|
||||
This creates one step that returns a hello message.
|
||||
|
||||
Steps can accept input parameters.
|
||||
|
||||
For example, add the following to `src/workflows/hello-world.ts`:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts"
|
||||
type WorkflowInput = {
|
||||
name: string
|
||||
}
|
||||
|
||||
const step2 = createStep("step-2", async ({ name }: WorkflowInput) => {
|
||||
return new StepResponse(`Hello ${name} from step two!`)
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Create a Workflow
|
||||
|
||||
Next, add the following to the same file to create the workflow using the `createWorkflow` function:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts"
|
||||
import {
|
||||
// other imports...
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
// ...
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
const str1 = step1()
|
||||
// to pass input
|
||||
const str2 = step2(input)
|
||||
|
||||
return new WorkflowResponse({
|
||||
message: str1,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default myWorkflow
|
||||
```
|
||||
|
||||
This creates a `hello-world` workflow. When you create a workflow, it’s constructed but not executed yet.
|
||||
|
||||
The workflow must return an instance of `WorkflowResponse`, whose first parameter is returned to workflow executors.
|
||||
|
||||
### 3. Execute the Workflow
|
||||
|
||||
You can execute a workflow from different resources within Medusa.
|
||||
|
||||
- Use API routes to execute the workflow in response to an API request or a webhook.
|
||||
- Use subscribers to execute a workflow when an event is triggered.
|
||||
- Use scheduled jobs to execute a workflow on a regular schedule.
|
||||
|
||||
To execute the workflow, invoke it passing the Medusa container as a parameter. Then, use its `run` method:
|
||||
|
||||
<CodeTabs group="resource-types">
|
||||
<CodeTab label="API Route" value="api-route">
|
||||
|
||||
```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
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)
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="Subscriber" value="subscriber">
|
||||
|
||||
```ts title="src/subscribers/customer-created.ts" highlights={[["20"], ["21"], ["22"], ["23"], ["24"], ["25"]]} collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
type SubscriberConfig,
|
||||
type SubscriberArgs,
|
||||
} from "@medusajs/framework"
|
||||
import myWorkflow from "../workflows/hello-world"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import { IUserModuleService } from "@medusajs/framework/types"
|
||||
|
||||
export default async function handleCustomerCreate({
|
||||
event: { data },
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const userId = data.id
|
||||
const userModuleService: IUserModuleService = container.resolve(
|
||||
Modules.USER
|
||||
)
|
||||
|
||||
const user = await userModuleService.retrieveUser(userId)
|
||||
|
||||
const { result } = await myWorkflow(container)
|
||||
.run({
|
||||
input: {
|
||||
name: user.first_name,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(result)
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: "user.created",
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="Scheduled Job" value="scheduled-job">
|
||||
|
||||
```ts title="src/jobs/message-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"]]}
|
||||
import { MedusaContainer } from "@medusajs/framework/types"
|
||||
import myWorkflow from "../workflows/hello-world"
|
||||
|
||||
export default async function myCustomJob(
|
||||
container: MedusaContainer
|
||||
) {
|
||||
const { result } = await myWorkflow(container)
|
||||
.run({
|
||||
input: {
|
||||
name: "John",
|
||||
},
|
||||
})
|
||||
|
||||
console.log(result.message)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
name: "run-once-a-day",
|
||||
schedule: `0 0 * * *`,
|
||||
};
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
### 4. Test Workflow
|
||||
|
||||
To test out your workflow, start your Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, send a `GET` request to `/workflow`:
|
||||
|
||||
```bash
|
||||
curl http://localhost:9000/workflow?name=john
|
||||
```
|
||||
|
||||
You’ll receive the following response:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Hello from step one!"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use Workflows
|
||||
|
||||
<Note title="Use workflows when" type="success">
|
||||
|
||||
You're implementing a custom feature exposed by an API route, or used in subscribers or scheduled jobs.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Resolve Resources
|
||||
|
||||
Each step in the workflow receives as a second parameter a `context` object. The object holds a `container` property which is the Medusa container. Use it to resolve other resources, such as services, of your Medusa application.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["12", "resolve", "Resolve the Product Module's main service."],
|
||||
[
|
||||
"12",
|
||||
"Modules.PRODUCT",
|
||||
"The resource registration name imported from `@medusajs/framework/utils`.",
|
||||
],
|
||||
]
|
||||
|
||||
```ts title="src/workflows/product-count.ts" highlights={highlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { IProductModuleService } from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
const step1 = createStep("step-1", async (_, context) => {
|
||||
const productModuleService: IProductModuleService =
|
||||
context.container.resolve(Modules.PRODUCT)
|
||||
|
||||
const [, count] = await productModuleService.listAndCountProducts()
|
||||
|
||||
return new StepResponse(count)
|
||||
})
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"product-count",
|
||||
function () {
|
||||
const count = step1()
|
||||
|
||||
return new WorkflowResponse({
|
||||
count,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default myWorkflow
|
||||
```
|
||||
|
||||
In the step, you resolve the Product Module's main service and use it to retrieve the product count.
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Create Brand API Route`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to define an API route that creates a brand as the last step of the ["Build Custom Features" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "createBrandWorkflow",
|
||||
link: "/customization/custom-features/workflow"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
Create the file `src/api/admin/brands/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/admin/brands/route.ts" collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import {
|
||||
CreateBrandInput,
|
||||
createBrandWorkflow,
|
||||
} from "../../../workflows/create-brand"
|
||||
|
||||
export const POST = async (
|
||||
req: MedusaRequest<CreateBrandInput>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const { result } = await createBrandWorkflow(req.scope)
|
||||
.run({
|
||||
input: req.body,
|
||||
})
|
||||
|
||||
res.json({ brand: result })
|
||||
}
|
||||
```
|
||||
|
||||
This adds a `POST` API route at `/admin/brands`. In the API route's handler, you execute the `createBrandWorkflow`, passing it the request body as input.
|
||||
|
||||
You return in the response the created brand.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about API routes [in this guide](../../../basics/api-routes/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Test API Route
|
||||
|
||||
To test it out, first, retrieve an authenticated token of your admin user by sending a `POST` request to the `/auth/user/emailpass` API Route:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/auth/user/emailpass' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"email": "admin@medusa-test.com",
|
||||
"password": "supersecret"
|
||||
}'
|
||||
```
|
||||
|
||||
Make sure to replace the email and password with your user's credentials.
|
||||
|
||||
Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/admin/brands' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer {token}' \
|
||||
--data '{
|
||||
"name": "Acme"
|
||||
}'
|
||||
```
|
||||
|
||||
This returns the created brand in the response:
|
||||
|
||||
```json title="Example Response"
|
||||
{
|
||||
"brand": {
|
||||
"id": "01J7AX9ES4X113HKY6C681KDZJ",
|
||||
"name": "Acme",
|
||||
"created_at": "2024-09-09T08:09:34.244Z",
|
||||
"updated_at": "2024-09-09T08:09:34.244Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand by:
|
||||
|
||||
1. Creating a module that defines and manages the `Brand` data model.
|
||||
2. Creating a workflow that uses the module's main service to create a brand record, and implements the compensation logic to delete that brand in case an error occurs.
|
||||
3. Creating an API route that allows admin users to create a brand.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
In the next chapters, you'll learn how to extend data models and associate the brand with a product.
|
||||
@@ -0,0 +1,128 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Implement Brand Module`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to create a Brand Module as part of the ["Build Custom Features" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## 1. Create Module Directory
|
||||
|
||||
Start by creating the directory `src/modules/brand` that will hold the Brand Module's files.
|
||||
|
||||
---
|
||||
|
||||
## 2. Create Data Model
|
||||
|
||||
To create a data model that represents a new `brand` table in the database, create the file `src/modules/brand/models/brand.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/brand/models/brand.ts"
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
export const Brand = model.define("brand", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text(),
|
||||
})
|
||||
```
|
||||
|
||||
This creates a `Brand` data model which has an `id` primary key property, and a `name` text property.
|
||||
|
||||
---
|
||||
|
||||
## 3. Create Module Service
|
||||
|
||||
Next, you'll create the module's main service that manages the `Brand` data model.
|
||||
|
||||
Create the file `src/modules/brand/service.ts` with the following content:
|
||||
|
||||
export const serviceHighlights = [
|
||||
["4", "MedusaService", "A service factory that generates data-management methods."]
|
||||
]
|
||||
|
||||
```ts title="src/modules/brand/service.ts" highlights={serviceHighlights}
|
||||
import { MedusaService } from "@medusajs/framework/utils"
|
||||
import { Brand } from "./models/brand"
|
||||
|
||||
class BrandModuleService extends MedusaService({
|
||||
Brand,
|
||||
}) {
|
||||
|
||||
}
|
||||
|
||||
export default BrandModuleService
|
||||
```
|
||||
|
||||
The `BrandModuleService` extends a `MedusaService` function imported from `@medusajs/framework/utils` which is a service factory.
|
||||
|
||||
The `MedusaService` function receives an object of the module's data models as a parameter, and generates methods to manage those data models, such as `createBrands` and `updateBrands`.
|
||||
|
||||
Those methods are now available at the `BrandModuleService` class and you'll use them in upcoming steps.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Find a reference of the generated methods in [this guide](!resources!/service-factory-reference).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## 4. Create Module's Definition
|
||||
|
||||
To export the module's definition, create the file `src/modules/brand/index.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/brand/index.ts"
|
||||
import { Module } from "@medusajs/framework/utils"
|
||||
import BrandModuleService from "./service"
|
||||
|
||||
export const BRAND_MODULE = "brandModuleService"
|
||||
|
||||
export default Module(BRAND_MODULE, {
|
||||
service: BrandModuleService,
|
||||
})
|
||||
```
|
||||
|
||||
This exposes the module to your application and allows you to resolve the `BrandModuleService`, which is its main service.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about modules and services [in this guide](../../../basics/modules/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## 5. Register Module in Config
|
||||
|
||||
Finally, add the module to Medusa's configurations in `medusa-config.ts`:
|
||||
|
||||
```ts title="medusa-config.ts"
|
||||
module.exports = defineConfig({
|
||||
// ...
|
||||
modules: [
|
||||
{
|
||||
resolve: "./src/modules/brand",
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Generate and Run Migrations
|
||||
|
||||
To reflect the data model in the database, generate migrations for the `brandModuleService` module and migrate the changes to the database:
|
||||
|
||||
```bash
|
||||
npx medusa db:generate brandModuleService
|
||||
npx medusa db:migrate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Step: Create Brand Workflow
|
||||
|
||||
In the next step, you'll create a workflow whose steps use the Brand Module's main service to create a brand.
|
||||
@@ -0,0 +1,25 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Build Custom Features`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about the concepts you need to build custom features in your Medusa application.
|
||||
|
||||
To add a custom feature to your application, you create:
|
||||
|
||||
1. A module with data models and a main service to manage them.
|
||||
2. A workflow to create, update, and delete records of data models. You implement functionalities in a workflow to benefit from features such as roll-back in case of errors, retry configurations, and more.
|
||||
3. An API route that exposes the workflow's functionality to clients, such as the storefront or admin dashboard.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Next Chapters: Brand Module Example
|
||||
|
||||
In the next chapters, you'll follow an example to:
|
||||
|
||||
1. Add a Brand Module that creates a `Brand` data model and provides data-management features.
|
||||
2. Add a workflow to create a brand.
|
||||
3. Expose an API route that allows admin users to create a brand using the workflow.
|
||||
@@ -0,0 +1,152 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Define Workflow to Create a Brand`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to define a workflow that creates a brand as part of the ["Build Custom Features" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## Workflows vs Services: Why use Workflows?
|
||||
|
||||
When manipulating data, use workflows instead of invoking a service's methods directly in your API route or other customizations.
|
||||
|
||||
Workflows eliminate data inconsistency in your application with its compensation mechanism that undoes changes if an error occurs. For example, if a workflow's step creates a brand, it also defines a compensation mechanism to remove the brand if an error occurs.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about workflows [in this guide](../../../basics/workflows/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
This is even more useful when you create workflows with many steps, or integrate third-party systems.
|
||||
|
||||
---
|
||||
|
||||
## Create createBrandWorkflow
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module",
|
||||
link: "/customization/custom-features/module"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
Create the file `src/workflows/create-brand/index.ts` with the following content:
|
||||
|
||||
```ts
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
export type CreateBrandInput = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export const createBrandWorkflow = createWorkflow(
|
||||
"create-brand",
|
||||
(input: CreateBrandInput) => {
|
||||
// TODO
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
For now, this workflow only defines its input. You'll create its step and use it in the workflow.
|
||||
|
||||
---
|
||||
|
||||
## Create createBrandStep
|
||||
|
||||
Create the file `src/workflows/create-brand/steps/create-brand.ts` with the following content:
|
||||
|
||||
```ts title="src/workflows/create-brand/steps/create-brand.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { CreateBrandInput } from ".."
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
|
||||
export const createBrandStep = createStep(
|
||||
"create-brand-step",
|
||||
async (input: CreateBrandInput, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
const brand = await brandModuleService.createBrands(input)
|
||||
|
||||
return new StepResponse(brand, brand.id)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This defines a `createBrandStep`. In the step, you resolve the Brand Module's main service and use its generated `createBrands` method, which accepts one or more objects of brands to create.
|
||||
|
||||
The step returns the created brand in the first parameter of the `StepResponse`'s constructor.
|
||||
|
||||
### Add Compensation Function to Step
|
||||
|
||||
A compensation function rolls back changes made by the step if an error occurs in the workflow.
|
||||
|
||||
The second parameter of the `StepResponse`'s constructor is passed to the compensation function.
|
||||
|
||||
To add the compensation function, pass a third parameter to `createStep`:
|
||||
|
||||
```ts title="src/workflows/create-brand/steps/create-brand.ts"
|
||||
export const createBrandStep = createStep(
|
||||
// ...
|
||||
async (id: string, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
await brandModuleService.deleteBrands(id)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
You resolve the Brand Module's main service and use its generated `deleteBrands` method to delete the brand created by the step.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
The `deleteBrands` method accepts an ID or an array of IDs of brands to delete.
|
||||
|
||||
</Note>
|
||||
|
||||
So, when an error occurs during the workflow, the brand that was created by the step is deleted to maintain data consistency.
|
||||
|
||||
---
|
||||
|
||||
## Add Step to Workflow
|
||||
|
||||
Go back to the workflow at `src/workflows/create-brand/index.ts` and import the step you created:
|
||||
|
||||
```ts
|
||||
import { createBrandStep } from "./steps/create-brand"
|
||||
```
|
||||
|
||||
Then, replace the `TODO` with the following:
|
||||
|
||||
```ts
|
||||
const brand = createBrandStep(input)
|
||||
|
||||
return new WorkflowResponse(brand)
|
||||
```
|
||||
|
||||
You use the `createBrandStep` to create the brand and return it in the workflow's response.
|
||||
|
||||
---
|
||||
|
||||
## Next Step: Create Brand API Route
|
||||
|
||||
In the next step, you'll create an API route that allows admin users to create a brand using this workflow.
|
||||
@@ -0,0 +1,10 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Customize Admin to Add Brands`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you'll continue with the brands example to learn how to customize the Medusa Admin to:
|
||||
|
||||
- Show a product's brand on its details page using a widget.
|
||||
- Add a page showing the list of brands in your application using a UI route.
|
||||
@@ -0,0 +1,196 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Create Brands List UI Route in Admin`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to create a UI route (or page) that shows your brands as a step of the ["Customize Admin" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## What is a UI Route?
|
||||
|
||||
A UI route is a React Component that adds a new page to your admin dashboard.
|
||||
|
||||
The UI Route can be shown in the sidebar or added as a nested page.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisite: Add Retrieve Brand API Route
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module",
|
||||
link: "/customization/custom-features/module"
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
Before adding the UI route, you need an API route that retrieves all brands.
|
||||
|
||||
Create the file `src/api/admin/brands/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/admin/brands/route.ts" collapsibleLines="1-7" expandMoreButton="Show Imports"
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const brandModuleService: BrandModuleService = req.scope.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
const limit = req.query.limit || 15
|
||||
const offset = req.query.offset || 0
|
||||
|
||||
const [brands, count] = await brandModuleService.listAndCountBrands({}, {
|
||||
skip: offset as number,
|
||||
take: limit as number,
|
||||
})
|
||||
|
||||
res.json({
|
||||
brands,
|
||||
count,
|
||||
limit,
|
||||
offset,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This adds a `GET` API route at `/admin/brands`.
|
||||
|
||||
In the API route, you resolve the Brand Module's main service and use its `listAndCountBrands` method to retrieve the list of brands with their total count.
|
||||
|
||||
This method accepts as a first parameter filters to apply on the retrieved data, and as a second parameter configurations for pagination.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about the `listAndCount` method and its parameters in [this reference](!resources!service-factory-reference/methods/listAndCount).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Add a UI Route to Show Brands
|
||||
|
||||
A UI route is created in a file named `page.tsx` under subdirectories of the `src/admin/routes` directory. The file’s default export must be the UI route’s React component.
|
||||
|
||||
To create a UI route that shows the list of brands, create the file `src/admin/routes/brands/page.tsx` with the following content:
|
||||
|
||||
export const uiRouteHighlights = [
|
||||
["7", "brands", "State variable to store the brands."],
|
||||
["12", "fetch", "Retrieve the brands from the custom API route."]
|
||||
]
|
||||
|
||||
```tsx title="src/admin/routes/brands/page.tsx" highlights={uiRouteHighlights}
|
||||
import { Table, Container, Heading } from "@medusajs/ui"
|
||||
import { useEffect, useState } from "react"
|
||||
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||
import { TagSolid } from "@medusajs/icons"
|
||||
|
||||
const BrandsPage = () => {
|
||||
const [brands, setBrands] = useState<
|
||||
Record<string, string>[]
|
||||
>([])
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/admin/brands`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ brands: brandsData }) => {
|
||||
setBrands(brandsData)
|
||||
})
|
||||
}, [])
|
||||
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">Brands</Heading>
|
||||
</div>
|
||||
<div className="flex h-full flex-col overflow-hidden !border-t-0">
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>ID</Table.HeaderCell>
|
||||
<Table.HeaderCell>Name</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{brands.map((brand) => (
|
||||
<Table.Row key={brand.id}>
|
||||
<Table.Cell>{brand.id}</Table.Cell>
|
||||
<Table.Cell>{brand.name}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default BrandsPage
|
||||
|
||||
// TODO export configuration
|
||||
```
|
||||
|
||||
This adds a new page in the admin at `http://localhost:9000/app/brands`.
|
||||
|
||||
In the UI route's component, you retrieve the brands from the `/admin/brands` API route. You show the brands in a table.
|
||||
|
||||
<Note>
|
||||
|
||||
Admin customizations can use the [Medusa UI package](!ui!) to align your customizations with the admin's design. Also, [this guide](!resources!/admin-components) includes examples of common components in the Medusa Admin.
|
||||
|
||||
</Note>
|
||||
|
||||
### Add UI Route to the Sidebar
|
||||
|
||||
To add the UI route to the sidebar, replace the `TODO` at the end of the file with the following:
|
||||
|
||||
```ts title="src/admin/routes/brands/page.tsx"
|
||||
export const config = defineRouteConfig({
|
||||
label: "Brands",
|
||||
icon: TagSolid,
|
||||
})
|
||||
```
|
||||
|
||||
You export a `config` variable defined using the `defineRouteConfig` utility.
|
||||
|
||||
This indicates that a new item should be added to the sidebar with the title `Brands` and an icon from the [Medusa Icons package](!ui!/icons/overview).
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test it out, start the Medusa application and login into the Medusa Admin.
|
||||
|
||||
You'll find a new "Brands" sidebar item. If you click on it, a new page opens showing the list of brands in your store.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
By following the examples of the previous chapters, you:
|
||||
|
||||
- Created a widget that showed the brand of a product in the Medusa Admin.
|
||||
- Created a UI route that showed the list of brands in the Medusa Admin.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
In the next chapters, you'll learn how to integrate third-party systems into your Medusa application to sync brands.
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Show Brand of Product in Admin`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to show the brand of a product in the Medusa Admin using a widget as a step of the ["Customize Admin" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## Widget to Show Brand in Product Details
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Retrieve Brand of Product API Route",
|
||||
link: "/customization/extend-models/query-linked-records"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
To create a widget that shows a product's brand in its details page, create the file `src/admin/widgets/product-brand.tsx` with the following content:
|
||||
|
||||
export const highlights = [
|
||||
["7", "data", "Receive the product's details as a prop"],
|
||||
["9", "brand", "A state variable to store the brand"],
|
||||
["19", "fetch", "Retrieve the brand of a product using the custom API route"],
|
||||
["41", "zone", "Show the widget at the top of the product details page."]
|
||||
]
|
||||
|
||||
```tsx title="src/admin/widgets/product-brand.tsx" highlights={highlights}
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { DetailWidgetProps, AdminProduct } from "@medusajs/framework/types"
|
||||
import { useEffect, useState } from "react"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
|
||||
const ProductBrandWidget = ({
|
||||
data,
|
||||
}: DetailWidgetProps<AdminProduct>) => {
|
||||
const [brand, setBrand] = useState<
|
||||
Record<string, string> | undefined
|
||||
>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
return
|
||||
}
|
||||
|
||||
fetch(`/admin/products/${data.id}/brand`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ brand }) => {
|
||||
setBrand(brand)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [loading])
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">Brand</Heading>
|
||||
</div>
|
||||
{loading && <span>Loading...</span>}
|
||||
{brand && <span>Name: {brand.name}</span>}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductBrandWidget
|
||||
```
|
||||
|
||||
This adds a widget at the top of the product's details page.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about widgets [in this guide](../../../basics/admin-customizations/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
Widgets created in a details page receive the targetted item in a `data` prop. So, the `ProductBrandWidget` receives the product's details in the `data` prop.
|
||||
|
||||
In the widget, you fetch the product's brand from the `/admin/products/:id/brand` API route and display it.
|
||||
|
||||
<Note>
|
||||
|
||||
Admin customizations can use the [Medusa UI package](!ui!) to align your customizations with the admin's design. Also, [this guide](!resources!/admin-components) includes examples of common components in the Medusa Admin.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
Start your Medusa application and go to a product's details page in the Medusa Admin, you'll find a new block at the top of the page showing the product's brand.
|
||||
|
||||
---
|
||||
|
||||
## Next Chapter: Add List of Brands Page
|
||||
|
||||
In the next chapter, you'll add a new page or UI route that displays the list of brands in your application.
|
||||
@@ -0,0 +1,115 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Create Links between Brand and Product Records`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to create a link between the records of the `Brand` and `Product` data models as a step of the ["Extend Models" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## What is the Remote Link?
|
||||
|
||||
The remote link is a class with utility methods to manage links between data models' records.
|
||||
|
||||
It’s registered in the Medusa container under the `ContainerRegistrationKeys.REMOTE_LINK` (`remoteLink`) registration name.
|
||||
|
||||
### Example: Create Link with Remote Link
|
||||
|
||||
For example, consider the following step:
|
||||
|
||||
export const stepHighlights = [
|
||||
["19", "resolve", "Resolve the remote link."],
|
||||
["23", "create", "Create a link between two records."]
|
||||
]
|
||||
|
||||
```ts highlights={stepHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
Modules,
|
||||
ContainerRegistrationKeys,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { BRAND_MODULE } from "../../modules/brand"
|
||||
|
||||
type LinkProductToBrandStepInput = {
|
||||
productId: string
|
||||
brandId: string
|
||||
}
|
||||
|
||||
export const linkProductToBrandStep = createStep(
|
||||
"link-product-to-brand",
|
||||
async ({ productId, brandId }: LinkProductToBrandStepInput, { container }) => {
|
||||
const remoteLink = container.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_LINK
|
||||
)
|
||||
|
||||
remoteLink.create({
|
||||
[Modules.PRODUCT]: {
|
||||
product_id: productId,
|
||||
},
|
||||
[BRAND_MODULE]: {
|
||||
brand_id: brandId,
|
||||
},
|
||||
})
|
||||
|
||||
return new StepResponse(undefined, {
|
||||
productId,
|
||||
brandId,
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this step, you resolve the remote link, then use its `create` method to create a link between product and brand records.
|
||||
|
||||
The `create` method accepts as a parameter an object whose properties are the names of each module, and the value is an object.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Use the `Modules` enum imported from `@medusajs/framework/utils` to for the commerce module's names.
|
||||
|
||||
</Note>
|
||||
|
||||
The value object has a property, which is the name of the data model (as specified in `model.define`'s first parameter) followed by `_id`, and its value is the ID of the record to link.
|
||||
|
||||
### Dismiss Link in Compensation
|
||||
|
||||
The above step can have the following compensation function that dismisses the link between the records:
|
||||
|
||||
export const compensationHighlights = [
|
||||
["4", "resolve", "Resolve the remote link."],
|
||||
["8", "dismiss", "Create a link between two records."]
|
||||
]
|
||||
|
||||
```ts highlights={compensationHighlights}
|
||||
export const linkProductToBrandStep = createStep(
|
||||
// ...
|
||||
async ({ productId, brandId }, { container }) => {
|
||||
const remoteLink = container.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_LINK
|
||||
)
|
||||
|
||||
remoteLink.dismiss({
|
||||
[Modules.PRODUCT]: {
|
||||
product_id: productId,
|
||||
},
|
||||
[BRAND_MODULE]: {
|
||||
brand_id: brandId,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `dismiss` method removes the link to dismiss between two records. Its parameter is the same as that of the `create` method.
|
||||
|
||||
---
|
||||
|
||||
## Next Step: Extend Create Product API Route
|
||||
|
||||
In the next step, you'll extend the Create Product API route to allow passing a brand ID, and link a product to a brand.
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Define Link Between a Brand and a Product`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to define a link between the `Brand` and `Product`data models as a step of the ["Extend Models" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## 1. Define the Link Between Product and Brand
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module having a Brand data model",
|
||||
link: "/customization/custom-features/module"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines and exports the link using the `defineLink` function imported from `@medusajs/framework/utils`.
|
||||
|
||||
So, create the file `src/links/product-brand.ts` with the following content:
|
||||
|
||||
export const highlights = [
|
||||
["7", "linkable", "Special `linkable` property that holds the linkable data models of `ProductModule`."],
|
||||
["10", "linkable", "Special `linkable` property that holds the linkable data models of `BrandModule`."],
|
||||
]
|
||||
|
||||
```ts title="src/links/product-brand.ts" highlights={highlights}
|
||||
import BrandModule from "../modules/brand"
|
||||
import ProductModule from "@medusajs/medusa/product"
|
||||
import { defineLink } from "@medusajs/framework/utils"
|
||||
|
||||
export default defineLink(
|
||||
{
|
||||
linkable: ProductModule.linkable.product,
|
||||
isList: true,
|
||||
},
|
||||
BrandModule.linkable.brand
|
||||
)
|
||||
```
|
||||
|
||||
The `defineLink` function accepts two parameters, each specifying the link configurations of each data model.
|
||||
|
||||
Modules have a special `linkable` property that holds the data models' link configurations.
|
||||
|
||||
`defineLink` accepts for each parameter either:
|
||||
|
||||
- The data model's link configuration;
|
||||
- Or an object that has two properties:
|
||||
- `linkable`: the link configuration of the data model.
|
||||
- `isList`: Whether many records of the data model can be linked to the other model.
|
||||
|
||||
So, in the above code snippet, you define a link between the `Product` and `Brand` data models. Since `isList` is enabled on the product's side, a brand can be associated with multiple products.
|
||||
|
||||
---
|
||||
|
||||
## 2. Sync the Link to the Database
|
||||
|
||||
To reflect your link in the database, run the `db:sync-links` command:
|
||||
|
||||
```bash
|
||||
npx medusa db:sync-links
|
||||
```
|
||||
|
||||
This creates a table for the link in the database. The table stores the IDs of linked brand and product records.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
You can also use the `db:migrate` command, which both runs the migrations and syncs the links.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Next: Link Brand and Product Records
|
||||
|
||||
In the next chapter, you'll learn how to associate brand and product records by creating a link between them.
|
||||
@@ -0,0 +1,228 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Extend Create Product API Route`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to extend the Create Product API route to link a product to a brand as a step of the ["Extend Models" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## Additional Data in API Routes
|
||||
|
||||
Some API routes, including the [Create Product API route](https://docs.medusajs.com/v2/api/admin#products_postproducts), accept an `additional_data` request body parameter.
|
||||
|
||||
It's useful when you want to pass custom data, such as the brand ID, then perform an action based on this data, such as link the brand to the product.
|
||||
|
||||
---
|
||||
|
||||
## 1. Allow Passing the Brand ID in Additional Data
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module",
|
||||
link: "/customization/custom-features/module"
|
||||
},
|
||||
{
|
||||
text: "Defined link between the Brand and Product data models.",
|
||||
link: "/customization/extend-models/define-link"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
Before passing custom properties in the `additional_data` parameter, you add the property to `additional_data`'s validation rules.
|
||||
|
||||
Create the file `src/api/middlewares.ts`, which is a special file that defines middlewares or validation rules of custom properties passed in the `additional_data` parameter:
|
||||
|
||||
```ts title="src/api/middlewares.ts"
|
||||
import { defineMiddlewares } from "@medusajs/medusa"
|
||||
import { z } from "zod"
|
||||
|
||||
export default defineMiddlewares({
|
||||
routes: [
|
||||
{
|
||||
matcher: "/admin/products",
|
||||
method: ["POST"],
|
||||
additionalDataValidator: {
|
||||
brand_id: z.string().optional(),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
You use [Zod](https://zod.dev/) to add a validation rule to the `additional_data` parameter indicating that it can include a `brand_id` property of type string.
|
||||
|
||||
### defineMiddleware Parameters
|
||||
|
||||
The `defineMiddlewares` function accepts an object having a `routes` property. Its value is an array of middleware route objects, each having the following properties:
|
||||
|
||||
- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. It must be compatible with [path-to-regexp](https://github.com/pillarjs/path-to-regexp).
|
||||
- `method`: An array of HTTP method to apply the middleware or additional data validation to. If not supplied, it's applied to all HTTP methods.
|
||||
- `additionalDataValidator`: An object of key-value pairs defining the validation rules for custom properties using [Zod](https://zod.dev/).
|
||||
|
||||
---
|
||||
|
||||
## 2. Link Brand to Product using Workflow Hook
|
||||
|
||||
A workflow hook is a point in a workflow where you can inject a step to perform a custom functionality. This is useful to perform custom action in an API route's workflow.
|
||||
|
||||
The [createProductsWorkflow](!resources!/references/medusa-workflows/createProductsWorkflow) used in the Create Product API route has a `productsCreated` hook that runs after the product is created.
|
||||
|
||||
So, to consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content:
|
||||
|
||||
export const hookHighlights = [
|
||||
["7", "productsCreated", "Access the hook in the `hooks` property."],
|
||||
["9", "", "Only proceed if the brand ID is passed in the additional data."],
|
||||
["18", "retrieveBrand", "Try to retrieve the brand to ensure it exists."],
|
||||
["27", "links", "Define an array to store the links in."],
|
||||
["31", "push", "Add a link to be created."],
|
||||
["41", "create", "Create the links."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/hooks/created-product.ts" highlights={hookHighlights}
|
||||
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
|
||||
import { StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||
import { BRAND_MODULE } from "../../modules/brand"
|
||||
import BrandModuleService from "../../modules/brand/service"
|
||||
|
||||
createProductsWorkflow.hooks.productsCreated(
|
||||
(async ({ products, additional_data }, { container }) => {
|
||||
if (!additional_data.brand_id) {
|
||||
return new StepResponse([], [])
|
||||
}
|
||||
|
||||
// check that brand exists
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
// if the brand doesn't exist, an error is thrown.
|
||||
await brandModuleService.retrieveBrand(additional_data.brand_id as string)
|
||||
|
||||
const remoteLink = container.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_LINK
|
||||
)
|
||||
const logger = container.resolve(
|
||||
ContainerRegistrationKeys.LOGGER
|
||||
)
|
||||
|
||||
const links = []
|
||||
|
||||
// link products to brands
|
||||
for (const product of products) {
|
||||
links.push({
|
||||
[Modules.PRODUCT]: {
|
||||
product_id: product.id,
|
||||
},
|
||||
[BRAND_MODULE]: {
|
||||
brand_id: additional_data.brand_id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await remoteLink.create(links)
|
||||
|
||||
logger.info("Linked brand to products")
|
||||
|
||||
return new StepResponse(links, links)
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
Workflows have a special `hooks` property to access its hooks and consume them. Each hook, such as `productCreated`, accept a step function as a parameter.
|
||||
|
||||
In the step, if a brand ID is passed in `additional_data` and the brand exists, you create a link between each product and the brand.
|
||||
|
||||
### Dismiss Links in Compensation
|
||||
|
||||
You can pass as a second parameter of the hook a compensation function that undoes what the step did.
|
||||
|
||||
Add the following compensation function as a second parameter:
|
||||
|
||||
```ts title="src/workflows/hooks/created-product.ts"
|
||||
createProductsWorkflow.hooks.productsCreated(
|
||||
// ...
|
||||
(async (links, { container }) => {
|
||||
if (!links.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const remoteLink = container.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_LINK
|
||||
)
|
||||
|
||||
await remoteLink.dismiss(links)
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
In the compensation function, you dismiss the links created by the step using the `dismiss` method of the remote link.
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test it out, first, retrieve the authentication token of your admin user by sending a `POST` request to `/auth/user/emailpass`:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/auth/user/emailpass' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"email": "admin@medusa-test.com",
|
||||
"password": "supersecret"
|
||||
}'
|
||||
```
|
||||
|
||||
Make sure to replace the email and password with your user's credentials.
|
||||
|
||||
Then, send a `POST` request to `/admin/products` to create a product, and pass in the `additional_data` parameter a brand's ID:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/admin/products' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer {token}' \
|
||||
--data '{
|
||||
"title": "Product 1",
|
||||
"options": [
|
||||
{
|
||||
"title": "Default option",
|
||||
"values": ["Default option value"]
|
||||
}
|
||||
],
|
||||
"additional_data": {
|
||||
"brand_id": "01J7AX9ES4X113HKY6C681KDZ2J"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Make sure to replace the `{token}` in the Authorization header with the token received from the previous request.
|
||||
|
||||
</Note>
|
||||
|
||||
In the request body, you pass in the `additional_data` parameter a `brand_id`.
|
||||
|
||||
The request creates a product and returns it.
|
||||
|
||||
In the Medusa application's logs, you'll find the message `Linked brand to products`, indicating that the workflow hook handler ran and linked the brand to the products.
|
||||
|
||||
---
|
||||
|
||||
## Worflows and API Routes References
|
||||
|
||||
Medusa exposes hooks in many of its workflows that you can consume to add custom logic.
|
||||
|
||||
The [Store](!api!/store) and [Admin](!api!/admin) API references indicate what workflows are used in each API routes. By clicking on the workflow, you access the [workflow's reference](!resources!/medusa-workflows-reference) where you can see the hooks available in the workflow.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps: Query Linked Records
|
||||
|
||||
In the next chapter, you'll learn how to query the brand linked to a product.
|
||||
@@ -0,0 +1,40 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} How to Extend Data Models`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about Medusa's alternative approach to extending data models.
|
||||
|
||||
## Extend Models Alternative: Module Links
|
||||
|
||||
Since modules are isolated from one another, it's not possible to directly extend a module's data models.
|
||||
|
||||
Instead, you define a link between the modules' data models.
|
||||
|
||||
### Why are Modules Isolated?
|
||||
|
||||
Some of the module isolation's benefits include:
|
||||
|
||||
- Integrate your module into any Medusa application without side-effects to your setup.
|
||||
- Replace existing modules with your custom implementation, if your use case is drastically different.
|
||||
- Use modules in other environments, such as Edge functions and Next.js apps.
|
||||
|
||||
### How does Medusa Manage Module Links?
|
||||
|
||||
When you define a link, the Medusa application creates a table in the database for it.
|
||||
|
||||
Then, when you create a link between two records, the Medusa application stores the IDs of the linked records in that table.
|
||||
|
||||
Medusa also provides the necessary tools to manage and query the linked records, which you'll learn about in the next chapters.
|
||||
|
||||
---
|
||||
|
||||
## Next Chapters: Link Brands to Products Example
|
||||
|
||||
The next chapters continue the brands example. It shows you how to:
|
||||
|
||||
- Link a brand, which you defined in a [previous example](../custom-features/module/page.mdx), to a product.
|
||||
- Manage linked records between the brands and products.
|
||||
- Extend Medusa's Create Product API route to link a product to a brand.
|
||||
- Query linked brands and products.
|
||||
@@ -0,0 +1,183 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Retrieve Brand linked to Product using Query`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to retrieve the brand linked to a product using Query as a step of the ["Extend Models" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## What is Query?
|
||||
|
||||
Query is a utility that retrieves data across modules and their links. It’s registered in the Medusa container under the `ContainerRegistrationKeys.QUERY` (`query`) registration name.
|
||||
|
||||
---
|
||||
|
||||
## Retrieve Brand of Product API Route
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module",
|
||||
link: "/customization/custom-features/module"
|
||||
},
|
||||
{
|
||||
text: "Defined link between the Brand and Product data models.",
|
||||
link: "/customization/extend-models/define-link"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
You'll create an API route that retrieves the brand of a product. You'll use this in a later chapter.
|
||||
|
||||
Create the file `src/api/admin/products/[id]/brand/route.ts` with the following content:
|
||||
|
||||
export const highlights = [
|
||||
["13", "resolve", "Resolve Query from the Medusa Container."],
|
||||
["17", "graph", "Run a query to retrieve a product by its ID and its brand."],
|
||||
["18", "entity", "The name of the model to query."],
|
||||
["19", "fields", "The fields and relations to retrieve."],
|
||||
["20", "filters", "The filters to apply on the retrieved data."]
|
||||
]
|
||||
|
||||
```ts title="src/api/admin/products/[id]/brand/route.ts" highlights={highlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
} from "@medusajs/framework/utils"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const query = req.scope.resolve(
|
||||
ContainerRegistrationKeys.QUERY
|
||||
)
|
||||
|
||||
const { data: [product] } = await query.graph({
|
||||
entity: "product",
|
||||
fields: ["brand.*"],
|
||||
filters: {
|
||||
id: req.params.id,
|
||||
},
|
||||
})
|
||||
|
||||
res.json({ brand: product.brand })
|
||||
}
|
||||
```
|
||||
|
||||
In this example, you retrieve a product by its ID with its brand, and return the brand in the response.
|
||||
|
||||
### query.graph Parameters
|
||||
|
||||
The `graph` method of Query runs a query to retrieve data. It accepts an object having the following properties:
|
||||
|
||||
- `entity`: The data model's name as specified in the first parameter of `model.define`.
|
||||
- `fields`: An array of properties and relations to retrieve. You can pass:
|
||||
- A property's name, such as `id`.
|
||||
- A relation or linked model's name, such as `brand`. You suffix the name with `.*` to retrieve all its properties.
|
||||
- `filters`: An object of filters to apply on the retrieved data model's properties.
|
||||
|
||||
<Note>
|
||||
|
||||
Filters currently don't work on models of another module, such as `brand` in this example.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test the API route out, first, retrieve the authentication token of your admin user by sending a `POST` request to `/auth/user/emailpass`:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/auth/user/emailpass' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"email": "admin@medusa-test.com",
|
||||
"password": "supersecret"
|
||||
}'
|
||||
```
|
||||
|
||||
Make sure to replace the email and password with your user's credentials.
|
||||
|
||||
Then, send a `GET` request to `/admin/products/:id/brand`:
|
||||
|
||||
```bash
|
||||
curl 'http://localhost:9000/admin/products/prod_123/brand' \
|
||||
-H 'Authorization: Bearer {token}'
|
||||
```
|
||||
|
||||
This returns the product's brand if it has one. For example:
|
||||
|
||||
```json title="Example Response"
|
||||
{
|
||||
"brand": {
|
||||
"id": "123",
|
||||
"name": "Acme",
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Retrieve Products of a Brand
|
||||
|
||||
An example of retrieving the products of a brand:
|
||||
|
||||
export const brandProductsHighlights = [
|
||||
["7", `"products.*"`, "Use the plural name of `product` since a brand has multiple products."]
|
||||
]
|
||||
|
||||
```ts highlights={brandProductsHighlights}
|
||||
const query = req.scope.resolve(
|
||||
ContainerRegistrationKeys.QUERY
|
||||
)
|
||||
|
||||
const { data: [brand] } = await query.graph({
|
||||
entity: "brand",
|
||||
fields: ["products.*"],
|
||||
filters: {
|
||||
id: req.params.id,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
In this case, since a brand has multiple products, you specify the plural name of the `Product` data model (`products`) in `fields`.
|
||||
|
||||
The retrieved `brand` now has a `products` field, which is an array of products linked to it:
|
||||
|
||||
```json title="Example Response"
|
||||
{
|
||||
"brand": {
|
||||
"products": [
|
||||
// ...
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
By following the examples of the previous chapters, you:
|
||||
|
||||
- Defined a link between the Brand and Product modules's data models, as if you're extending the `Product` model to add a brand.
|
||||
- Created a link between brand and product records.
|
||||
- Queried the brand linked to a product, and vice versa.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
In the next chapters, you'll learn how to customize the Medusa Admin to show brands.
|
||||
@@ -0,0 +1,213 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Brand Example: Handle Event to Sync Third-Party System`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to emit an event when a brand is created, listen to that event in a subscriber, and create the brand in the third-party system as a step of the ["Integrate Systems" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## 1. Emit Custom Event for Brand Creation
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module with createBrandWorkflow",
|
||||
link: "/customization/custom-features/workflow"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
To handle brand-creation event, you'll emit a custom event when a brand is created.
|
||||
|
||||
In the `createBrandWorkflow` defined in `src/workflows/create-brand/index.ts`, use the `emitEventStep` helper step imported from `@medusajs/medusa/core-flows` after the `createBrandStep`:
|
||||
|
||||
export const eventHighlights = [
|
||||
["13", "emitEventStep", "Emit an event."],
|
||||
["14", "eventName", "The event's name."],
|
||||
["15", "data", "The data to pass in the payload."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/create-brand/index.ts" highlights={eventHighlights}
|
||||
// other imports...
|
||||
import {
|
||||
emitEventStep,
|
||||
} from "@medusajs/medusa/core-flows"
|
||||
|
||||
// ...
|
||||
|
||||
export const createBrandWorkflow = createWorkflow(
|
||||
"create-brand",
|
||||
(input: CreateBrandInput) => {
|
||||
// ...
|
||||
|
||||
emitEventStep({
|
||||
eventName: "brand.created",
|
||||
data: {
|
||||
id: brand.id,
|
||||
},
|
||||
})
|
||||
|
||||
return new WorkflowResponse(brand)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `emitEventStep` accepts as a parameter an object having two properties:
|
||||
|
||||
- `eventName`: The name of the event to emit.
|
||||
- `data`: The data payload to emit with the event. This is useful for subscribers to access the created brand.
|
||||
|
||||
---
|
||||
|
||||
## 2. Create Sync to Third-Party System Workflow
|
||||
|
||||
Next, you'll create the workflow that syncs the created brand to the third-party system.
|
||||
|
||||
Create the file `src/workflows/sync-brand-to-system/index.ts` with the following content:
|
||||
|
||||
```ts title="src/workflows/sync-brand-to-system/index.ts"
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
export type SyncBrandToSystemInput = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const syncBrandToSystemWorkflow = createWorkflow(
|
||||
"sync-brand-to-system",
|
||||
(input: SyncBrandToSystemInput) => {
|
||||
// ...
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This defines an empty workflow and its expected input.
|
||||
|
||||
### Create createBrandInSystemStep
|
||||
|
||||
Next, create the step that syncs the brand in the file `src/workflows/sync-brand-to-system/steps/create-brand-in-system.ts`:
|
||||
|
||||
export const stepHighlights = [
|
||||
["18", "createBrand", "Create a brand in the third-party system."],
|
||||
["27", "deleteBrand", "Delete the brand in the third-party system if an error occurs."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/sync-brand-to-system/steps/create-brand-in-system.ts" highlights={stepHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { SyncBrandToSystemInput } from ".."
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
|
||||
export const createBrandInSystemStep = createStep(
|
||||
"create-brand-in-system",
|
||||
async ({ id }: SyncBrandToSystemInput, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
const brand = await brandModuleService.retrieveBrand(id)
|
||||
|
||||
await brandModuleService.client.createBrand(brand)
|
||||
|
||||
return new StepResponse(null, brand.id)
|
||||
},
|
||||
async (id, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
await brandModuleService.client.deleteBrand(id)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This step resolves the Brand Module's main service and uses its `client` property to access its internal service that integrates the third-party system.
|
||||
|
||||
In the step, you use the `createBrand` method of the client to create the brand in the third-party system.
|
||||
|
||||
In the compensation function, you undo the step's action using the `deleteBrand` method of the client.
|
||||
|
||||
### Add Step to Workflow
|
||||
|
||||
Finally, add the step to the `syncBrandToSystemWorkflow` in `src/workflows/sync-brand-to-system/index.ts`:
|
||||
|
||||
```ts title="src/workflows/sync-brand-to-system/index.ts"
|
||||
// other imports...
|
||||
import { createBrandInSystemStep } from "./steps/create-brand-in-system"
|
||||
|
||||
// ...
|
||||
|
||||
export const syncBrandToSystemWorkflow = createWorkflow(
|
||||
"sync-brand-to-system",
|
||||
(input: SyncBrandToSystemInput) => {
|
||||
createBrandInSystemStep(input)
|
||||
|
||||
return new WorkflowResponse(undefined)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The workflow now calls the step and returns an `undefined` result.
|
||||
|
||||
---
|
||||
|
||||
## 3. Handle brand.created Event
|
||||
|
||||
To handle the `brand.created` event, create a subscriber at `src/subscribers/brand-created.ts` with the following content:
|
||||
|
||||
```ts title="src/subscribers/brand-created.ts"
|
||||
import type {
|
||||
SubscriberConfig,
|
||||
SubscriberArgs,
|
||||
} from "@medusajs/framework"
|
||||
import { syncBrandToSystemWorkflow } from "../workflows/sync-brand-to-system"
|
||||
|
||||
export default async function brandCreatedHandler({
|
||||
event: { data },
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
await syncBrandToSystemWorkflow(container).run({
|
||||
input: data,
|
||||
})
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: "brand.created",
|
||||
}
|
||||
```
|
||||
|
||||
The subscriber handler accesses the event payload in the `event.data` property of its object parameter.
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about subscribers [in this guide](../../../basics/events-and-subscribers/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
It then executes the `syncBrandToSystemWorkflow`, passing it the ID of the brand to create in the third-party system.
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test it out, start the Medusa application and create a brand using the API route created in a [previous chapter](../../custom-features/api-route/page.mdx#test-api-route).
|
||||
|
||||
If you check the logs, you'll find the `brand.created` event was emitted, and that the request to the third-party system was simulated.
|
||||
|
||||
---
|
||||
|
||||
## Next Chapter: Sync Brand from Third-Party System to Medusa
|
||||
|
||||
In the next chapter, you'll learn how to sync brands in the third-party system into Medusa using a workflow and a scheduled job.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Integrate Third-Party Systems`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to integrate a third-party system into Medusa.
|
||||
|
||||
## How to Integrate a Third-Party System?
|
||||
|
||||
To integrate a third-party system into Medusa, you:
|
||||
|
||||
1. Implement the methods to interact with the system in a service. It can either be the main module's service, or an internal service in the module that's used by the main one.
|
||||
2. Implement in workflows custom features around the integration, such as sending data to the third-party system.
|
||||
- Workflows roll-back mechanism ensures data consistency. This is essential as you integrate multiple systems into your application.
|
||||
3. Use the workflow in other resources to expose or utilize the custom functionality.
|
||||
|
||||
---
|
||||
|
||||
## Next Chapters: Syncing Brands Example
|
||||
|
||||
In the next chapters, you'll implement an example of syncing brands with a third-party system, such as a Content Management System (CMS).
|
||||
|
||||
That requires:
|
||||
|
||||
1. Implementing the service that integrates the third-party system.
|
||||
2. Creating a brand in the third-party system when a brand is created in Medusa.
|
||||
2. Retrieving the brands from the third-party system to sync them with Medusa's brands at a scheduled interval.
|
||||
@@ -0,0 +1,321 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Schedule Syncing Brands from Third-Party System`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to use workflows and scheduled jobs to sync brands from the third-party system as the last step of the ["Integrate Systems" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## 1. Implement Syncing Workflow
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module",
|
||||
link: "/customization/custom-features/module"
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
Start by defining the workflow that syncs the brand from the third-party system.
|
||||
|
||||
The workflow has the following steps:
|
||||
|
||||
1. Retrieve brands from the third-party system.
|
||||
2. Create new brands in Medusa.
|
||||
3. Update existing brands in Medusa.
|
||||
|
||||
### Retrieve Brands Step
|
||||
|
||||
To create the step that retrieves the brands from the third-party service, create the file `src/workflows/sync-brands-from-system/steps/retrieve-brands-from-system.ts` with the following content:
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/steps/retrieve-brands-from-system.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
|
||||
export const retrieveBrandsFromSystemStep = createStep(
|
||||
"retrieve-brands-from-system",
|
||||
async (_, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
const brands = await brandModuleService.client.retrieveBrands()
|
||||
|
||||
return new StepResponse(brands)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
In this step, you resolve the Brand Module's main service from the container, and use its client service to retrieve the brands from the third-party system.
|
||||
|
||||
The step returns the retrieved brands.
|
||||
|
||||
### Create Brands Step
|
||||
|
||||
Next, create the step that creates new brands in Medusa in the file `src/workflows/sync-brands-from-system/steps/create-brands.ts`:
|
||||
|
||||
export const createBrandsHighlights = [
|
||||
["21", "createBrands", "Create the brands in Medusa"],
|
||||
["30", "deleteBrands", "Delete the brands from Medusa"]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/steps/create-brands.ts" highlights={createBrandsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { InferTypeOf } from "@medusajs/framework/types"
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
import { Brand } from "../../../modules/brand/models/brand"
|
||||
|
||||
type CreateBrandsInput = {
|
||||
brands: InferTypeOf<typeof Brand>[]
|
||||
}
|
||||
|
||||
export const createBrandsStep = createStep(
|
||||
"create-brand-step",
|
||||
async (input: CreateBrandsInput, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
const brands = await brandModuleService.createBrands(input.brands)
|
||||
|
||||
return new StepResponse(brands, brands.map((brand) => brand.id))
|
||||
},
|
||||
async (ids: string[], { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
await brandModuleService.deleteBrands(ids)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This step receives the brands to create as input.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Since a data model is a variable, use the `InferTypeOf` utility imported from `@medusajs/framework/types` to infer its type.
|
||||
|
||||
</Note>
|
||||
|
||||
In the step, you resolve the Brand Module's main service and uses its `createBrands` method to create the brands.
|
||||
|
||||
You return the created brands and pass their IDs to the compensation function, which deletes the brands if an error occurs.
|
||||
|
||||
### Update Brands Step
|
||||
|
||||
To create the step that updates existing brands in Medusa, create the file `src/workflows/sync-brands-from-system/steps/update-brands.ts` with the following content:
|
||||
|
||||
export const updateBrandsHighlights = [
|
||||
["21", "prevUpdatedBrands", "Retrieve the data of the brands before the update."],
|
||||
["25", "updateBrands", "Update the brands in Medusa."],
|
||||
["34", "updateBrands", "Revert the update by reverting the brands' to before the update."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/steps/update-brands.ts" highlights={updateBrandsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { InferTypeOf } from "@medusajs/framework/types"
|
||||
import BrandModuleService from "../../../modules/brand/service"
|
||||
import { BRAND_MODULE } from "../../../modules/brand"
|
||||
import { Brand } from "../../../modules/brand/models/brand"
|
||||
|
||||
type UpdateBrandsInput = {
|
||||
brands: InferTypeOf<typeof Brand>[]
|
||||
}
|
||||
|
||||
export const updateBrandsStep = createStep(
|
||||
"update-brand-step",
|
||||
async ({ brands }: UpdateBrandsInput, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
const prevUpdatedBrands = await brandModuleService.listBrands({
|
||||
id: brands.map((brand) => brand.id),
|
||||
})
|
||||
|
||||
const updatedBrands = await brandModuleService.updateBrands(brands)
|
||||
|
||||
return new StepResponse(updatedBrands, prevUpdatedBrands)
|
||||
},
|
||||
async (prevUpdatedBrands, { container }) => {
|
||||
const brandModuleService: BrandModuleService = container.resolve(
|
||||
BRAND_MODULE
|
||||
)
|
||||
|
||||
await brandModuleService.updateBrands(prevUpdatedBrands)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This step receives the brands to update as input.
|
||||
|
||||
In the step, you retrieve the brands first to pass them later to the compensation function, then update and return the brands.
|
||||
|
||||
In the compensation function, you update the brands are again but to their data before the update made by the step.
|
||||
|
||||
### Create Workflow
|
||||
|
||||
Finally, create the workflow in the file `src/workflows/sync-brands-from-system/index.ts` with the following content:
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/index.ts"
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
transform,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { InferTypeOf } from "@medusajs/framework/types"
|
||||
import { retrieveBrandsFromSystemStep } from "./steps/retrieve-brands-from-system"
|
||||
import { createBrandsStep } from "./steps/create-brands"
|
||||
import { updateBrandsStep } from "./steps/update-brands"
|
||||
import { Brand } from "../../modules/brand/models/brand"
|
||||
|
||||
export const syncBrandsFromSystemWorkflow = createWorkflow(
|
||||
"sync-brands-from-system",
|
||||
() => {
|
||||
const brands = retrieveBrandsFromSystemStep()
|
||||
|
||||
// TODO create and update brands
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
For now, you only add the `retrieveBrandsFromSystemStep` to the workflow that retrieves the brands from the third-party system.
|
||||
|
||||
### Identify Brands to Create or Update in Workflow
|
||||
|
||||
Next, you need to identify which brands must be created or updated.
|
||||
|
||||
Since workflows are constructed internally and are only evaluated during execution, you can't access any data's value to perform data manipulation or checks.
|
||||
|
||||
Instead, use the `transform` utility function imported from `@medusajs/framework/workflows-sdk`, which gives you access to the real-time values of the data to perform actions on them.
|
||||
|
||||
So, replace the `TODO` with the following:
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/index.ts"
|
||||
const { toCreate, toUpdate } = transform(
|
||||
{
|
||||
brands,
|
||||
},
|
||||
(data) => {
|
||||
const toCreate: InferTypeOf<typeof Brand>[] = []
|
||||
const toUpdate: InferTypeOf<typeof Brand>[] = []
|
||||
|
||||
data.brands.forEach((brand) => {
|
||||
if (brand.external_id) {
|
||||
toUpdate.push({
|
||||
...brand,
|
||||
id: brand.external_id,
|
||||
})
|
||||
} else {
|
||||
toCreate.push(brand)
|
||||
}
|
||||
})
|
||||
|
||||
return { toCreate, toUpdate }
|
||||
}
|
||||
)
|
||||
|
||||
// TODO create and update the brands
|
||||
```
|
||||
|
||||
`transform` accepts two parameters:
|
||||
|
||||
1. The data to be passed to the function in the second parameter.
|
||||
2. A function to execute only when the workflow is executed. Its return value can be consumed by the rest of the workflow.
|
||||
|
||||
In the function, you sort the brands as to be created or to be updated based on whether they have an `external_id` property.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
This approach assumes that the third-party system stores the ID of the brand in Medusa in `external_id`.
|
||||
|
||||
</Note>
|
||||
|
||||
### Create and Update the Brands
|
||||
|
||||
Finally, replace the new `TODO` with the following:
|
||||
|
||||
```ts title="src/workflows/sync-brands-from-system/index.ts"
|
||||
const created = createBrandsStep({ brands: toCreate })
|
||||
const updated = updateBrandsStep({ brands: toUpdate })
|
||||
|
||||
return new WorkflowResponse({
|
||||
created,
|
||||
updated,
|
||||
})
|
||||
```
|
||||
|
||||
You pass the brands to be created to the `createBrandsStep`, and the brands to be updated to the `updateBrandsStep`.
|
||||
|
||||
Then, you return the created and updated brands.
|
||||
|
||||
---
|
||||
|
||||
## 2. Schedule Syncing Task
|
||||
|
||||
To schedule a task that syncs brands from the third-party system, create a scheduled job at `src/jobs/sync-brands-from-system.ts`:
|
||||
|
||||
```ts title="src/jobs/sync-brands-from-system.ts"
|
||||
import { MedusaContainer } from "@medusajs/framework/types"
|
||||
import { syncBrandsFromSystemWorkflow } from "../workflows/sync-brands-from-system"
|
||||
|
||||
export default async function (container: MedusaContainer) {
|
||||
const logger = container.resolve("logger")
|
||||
|
||||
const { result } = await syncBrandsFromSystemWorkflow(container).run()
|
||||
|
||||
logger.info(
|
||||
`Synced brands from third-party system: ${
|
||||
result.created.length
|
||||
} brands created and ${result.updated.length} brands updated.`)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
name: "sync-brands-from-system",
|
||||
schedule: "* * * * *",
|
||||
}
|
||||
```
|
||||
|
||||
This defines a scheduled job that runs every minute (for testing purposes).
|
||||
|
||||
<Note>
|
||||
|
||||
Learn more about scheduled jobs [in this guide](../../../basics/scheduled-jobs/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
The scheduled job executes the `syncBrandsFromSystemWorkflow` and prints how many brands were created and updated.
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test it out, start the Medusa application. In a minute, the scheduled job will run and you'll see a logged message indicating how many brands were created or updated.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
In the previous chapters, you:
|
||||
|
||||
- Created a service that acts as a client integrating a third-party system.
|
||||
- Implemented two-way sync of brands between the third-party system and Medusa using a subscriber and a scheduled job.
|
||||
@@ -0,0 +1,205 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Integrate Third-Party Brand System in a Service`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note title="Example Chapter">
|
||||
|
||||
This chapter covers how to integrate a dummy third-party system in a service as a step of the ["Integrate Systems" chapter](../page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
## 1. Create Service
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Brand Module",
|
||||
link: "/customization/custom-features/module"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
Start by creating the file `src/modules/brand/services/client.ts` with the following content:
|
||||
|
||||
export const serviceHighlights = [
|
||||
["4", "BrandClientOptions", "Define the options that the Brand Module receives necessary for the integration."],
|
||||
["8", "InjectedDependencies", "Define the dependencies injected into the service."],
|
||||
["20", "moduleDef", "Retrieve the module's configuration."]
|
||||
]
|
||||
|
||||
```ts title="src/modules/brand/services/client.ts" highlights={serviceHighlights}
|
||||
import { Logger, ConfigModule } from "@medusajs/framework/types"
|
||||
import { BRAND_MODULE } from ".."
|
||||
|
||||
export type BrandClientOptions = {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
type InjectedDependencies = {
|
||||
logger: Logger
|
||||
configModule: ConfigModule
|
||||
}
|
||||
|
||||
export class BrandClient {
|
||||
private options_: BrandClientOptions
|
||||
private logger_: Logger
|
||||
|
||||
constructor({ logger, configModule }: InjectedDependencies) {
|
||||
this.logger_ = logger
|
||||
|
||||
const moduleDef = configModule.modules[BRAND_MODULE]
|
||||
if (typeof moduleDef !== "boolean") {
|
||||
this.options_ = moduleDef.options as BrandClientOptions
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This creates a `BrandClient` service. Using dependency injection, you resolve the `logger` and `configModule` from the Module's container.
|
||||
|
||||
`logger` is useful to log messages, and `configModule` has configurations exported in `medusa-config.ts`.
|
||||
|
||||
You also define an `options_` property in your service to store the module's options.
|
||||
|
||||
The `configModule`'s `modules` property is an object whose keys are registered module names and values are the module's configuration.
|
||||
|
||||
If the module's configuration isn't a boolean, it has an `options` property that holds the module's options. You use it to set the `options_` property's value.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
If the service integrating the third-party system was a main service, it receives the module's options as a second parameter.
|
||||
|
||||
</Note>
|
||||
|
||||
### Integration Methods
|
||||
|
||||
Next, add the following methods to simulate sending requests to the third-party system:
|
||||
|
||||
export const methodsHighlights = [
|
||||
["10", "sendRequest", "Since the third-party system isn't real, this method only logs a message."],
|
||||
["19", "createBrand", "A method that creates a brand in the third-party system."],
|
||||
["23", "deleteBrand", "A method that deletes a brand in the third-party system."],
|
||||
["27", "retrieveBrands", "A method that retrieves a brand from a third-party system."]
|
||||
]
|
||||
|
||||
```ts title="src/modules/brand/services/client.ts" highlights={methodsHighlights}
|
||||
// other imports...
|
||||
import { InferTypeOf } from "@medusajs/framework/types"
|
||||
import { Brand } from "../models/brand"
|
||||
|
||||
export class BrandClient {
|
||||
// ...
|
||||
|
||||
// a dummy method to simulate sending a request,
|
||||
// in a realistic scenario, you'd use an SDK, fetch, or axios clients
|
||||
private async sendRequest(url: string, method: string, data?: any) {
|
||||
this.logger_.info(`Sending a ${
|
||||
method
|
||||
} request to ${url}. data: ${JSON.stringify(data, null, 2)}`)
|
||||
this.logger_.info(`Client Options: ${
|
||||
JSON.stringify(this.options_, null, 2)
|
||||
}`)
|
||||
}
|
||||
|
||||
async createBrand(brand: InferTypeOf<typeof Brand>) {
|
||||
await this.sendRequest("/brands", "POST", brand)
|
||||
}
|
||||
|
||||
async deleteBrand(id: string) {
|
||||
await this.sendRequest(`/brands/${id}`, "DELETE")
|
||||
}
|
||||
|
||||
async retrieveBrands() {
|
||||
await this.sendRequest("/brands", "GET")
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `sendRequest` method is a dummy method to simulate sending a request to a third-party system.
|
||||
|
||||
You also add three methods that use the `sendRequest` method:
|
||||
|
||||
- `createBrand` that creates a brand in the third-party system. To reference a brand's type, you use the `InferTypeOf` utility imported from `@medusajs/framework/types`. This transforms a data model, which is a variable, to its equivalent type.
|
||||
- `deleteBrand` that deletes the brand in the third-party system.
|
||||
- `retrieveBrands` to retrieve a brand from the third-party system.
|
||||
|
||||
---
|
||||
|
||||
## 2. Export Service
|
||||
|
||||
If the service integrating the third-party system is the module's main service, you only need to export it in the module definition.
|
||||
|
||||
However, since this service is an internal service in the Brand Module, you must export it in a `src/modules/brand/services/index.ts` file:
|
||||
|
||||
```ts title="src/modules/brand/services/index.ts"
|
||||
export * from "./client"
|
||||
```
|
||||
|
||||
This registers the service in the module's container, allowing you to access it in the module's main service.
|
||||
|
||||
---
|
||||
|
||||
## 3. Add Internal Service in Main Service
|
||||
|
||||
In the main service at `src/modules/brand/service.ts`, add the following imports and types at the top of the file:
|
||||
|
||||
```ts title="src/modules/brand/service.ts"
|
||||
// other imports...
|
||||
import { BrandClient, BrandClientOptions } from "./services"
|
||||
|
||||
type InjectedDependencies = {
|
||||
brandClient: BrandClient
|
||||
}
|
||||
```
|
||||
|
||||
Then, add the following in the `BrandModuleService` class:
|
||||
|
||||
```ts title="src/modules/brand/service.ts"
|
||||
class BrandModuleService extends MedusaService({
|
||||
Brand,
|
||||
}) {
|
||||
public client: BrandClient
|
||||
|
||||
constructor({ brandClient }: InjectedDependencies) {
|
||||
super(...arguments)
|
||||
|
||||
this.client = brandClient
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the main module service, you first resolve through dependency injection the `brandClient` from the container and set it in a public property `client`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Pass Options to the Module
|
||||
|
||||
To pass options in the module, change its configurations in `medusa-config.ts`:
|
||||
|
||||
```ts title="medusa-config.ts"
|
||||
module.exports = defineConfig({
|
||||
// ...
|
||||
modules: [
|
||||
{
|
||||
resolve: "./src/modules/brand",
|
||||
options: {
|
||||
apiKey: process.env.BRAND_API_KEY || "temp",
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
A module's configuration accepts an `options` property, which can hold any options to pass to the module.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps: Sync Brand From Medusa to Third-Party System
|
||||
|
||||
In the next chapter, you'll learn how to sync brands created in Medusa to the third-party system using a workflow and a scheduled job.
|
||||
@@ -0,0 +1,38 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Customizations Next Steps`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
The previous examples under the Customization chapter explained more about customizing Medusa for a realistic use case.
|
||||
|
||||
Your learning journey doesn't end here, and this only presents some of Medusa's powerful feature.
|
||||
|
||||
This chapter guides you into how to continue your learning journey, and what resources will be helpful for you during your development.
|
||||
|
||||
## Follow the Rest of this Documentation
|
||||
|
||||
The next chapters of the documentation provide more in-depth uses of the different concepts you learned about.
|
||||
|
||||
While you can start playing around with Medusa and customize it, it's highly recommended to continue the rest of this documentation to learn about what more you can do with each concept.
|
||||
|
||||
---
|
||||
|
||||
## Helpful Resources Guides
|
||||
|
||||
The [Development Resources](!resources!) documentation provides more helpful guides and references for your development journey.
|
||||
|
||||
Some of these guides and references are:
|
||||
|
||||
1. [Service Factory Reference](!resources!/service-factory-reference): Learn about the methods generated by the service factory with examples.
|
||||
2. [Workflows Reference](!resources!/medusa-workflows-reference): Browse the list of workflows and their hooks.
|
||||
3. [Commerce Modules](!resources!/commerce-modules): Browse the list of commerce modules in Medusa and their references to learn how to use them.
|
||||
4. [Admin Injection Zones](!resources!/admin-widget-injection-zones): Browse the injection zones in the Medusa Admin to learn where you can inject widgets.
|
||||
|
||||
---
|
||||
|
||||
## More Examples in Recipes
|
||||
|
||||
In the Development Resources documentation, you'll also find step-by-step guides of different use cases, such as building a marketplace, digital products, and more.
|
||||
|
||||
Refer to the [Recipes](!resources!/recipes) documentation to learn more.
|
||||
@@ -0,0 +1,14 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Custom Development`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you’ll customize Medusa using the concepts you learned.
|
||||
|
||||
You'll follow an example of building a Brands Module that leads you through:
|
||||
|
||||
1. The main concepts you need to build custom features.
|
||||
2. How to extend data models that are in the Commerce Modules.
|
||||
3. How to use your custom features in pages and widgets in the admin dashboard.
|
||||
4. How to integrate third-party systems into your Medusa application.
|
||||
@@ -0,0 +1,156 @@
|
||||
import { Prerequisites, TypeList } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Configure Instrumentation`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry.
|
||||
|
||||
## Observability with OpenTelemtry
|
||||
|
||||
Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for:
|
||||
|
||||
- HTTP requests
|
||||
- Workflow executions
|
||||
- Query usages
|
||||
- Database queries and operations
|
||||
|
||||
---
|
||||
|
||||
## How to Configure Instrumentation in Medusa?
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "An exporter to visualize your application's traces, such as Zipkin.",
|
||||
link: "https://zipkin.io/pages/quickstart.html"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
Start by installing the following OpenTelemetry dependencies in your Medusa project:
|
||||
|
||||
```bash npm2yarn
|
||||
npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg
|
||||
```
|
||||
|
||||
Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies:
|
||||
|
||||
```bash npm2yarn
|
||||
npm install @opentelemetry/exporter-zipkin
|
||||
```
|
||||
|
||||
### Add instrumentation.ts
|
||||
|
||||
Next, create the file `instrumentation.ts` with the following content:
|
||||
|
||||
```ts title="instrumentation.ts"
|
||||
import { registerOtel } from "@medusajs/medusa"
|
||||
import { ZipkinExporter } from "@opentelemetry/exporter-zipkin"
|
||||
|
||||
// If using an exporter other than Zipkin, initialize it here.
|
||||
const exporter = new ZipkinExporter({
|
||||
serviceName: "my-medusa-project",
|
||||
})
|
||||
|
||||
export function register() {
|
||||
registerOtel({
|
||||
serviceName: "medusajs",
|
||||
// pass exporter
|
||||
exporter,
|
||||
instrument: {
|
||||
http: true,
|
||||
workflows: true,
|
||||
query: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
In the `instrumentation.ts` file, you export a `register` function that uses Medusa's `registerOtel` utility function.
|
||||
|
||||
You also initialize an instance of the exporter, such as Zipkin, and pass it to the `registerOtel` function.
|
||||
|
||||
The `registerOtel` utility function accepts an object having the following properties:
|
||||
|
||||
<TypeList
|
||||
types={[
|
||||
{
|
||||
name: "serviceName",
|
||||
type: "`string`",
|
||||
description: "The name of the service traced.",
|
||||
optional: false
|
||||
},
|
||||
{
|
||||
name: "exporter",
|
||||
type: "[SpanExporter](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_trace_base.SpanExporter.html)",
|
||||
description: "An instance of an exporter, such as Zipkin."
|
||||
},
|
||||
{
|
||||
name: "instrument",
|
||||
type: "`object`",
|
||||
description: "Options specifying what to trace.",
|
||||
optional: true,
|
||||
children: [
|
||||
{
|
||||
name: "http",
|
||||
type: "`boolean`",
|
||||
description: "Whether to trace HTTP requests."
|
||||
},
|
||||
{
|
||||
name: "query",
|
||||
type: "`boolean`",
|
||||
description: "Whether to trace Query usages."
|
||||
},
|
||||
{
|
||||
name: "workflows",
|
||||
type: "`boolean`",
|
||||
description: "Whether to trace Workflow executions."
|
||||
},
|
||||
{
|
||||
name: "db",
|
||||
type: "`boolean`",
|
||||
description: "Whether to trace database queries and operations."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "instrumentations",
|
||||
type: "[Instrumentation[]](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_instrumentation.Instrumentation.html)",
|
||||
description: "Additional instrumentation options that OpenTelemetry accepts.",
|
||||
optional: true
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test it out, start your exporter, such as Zipkin.
|
||||
|
||||
Then, start your Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Try to open the Medusa Admin or send a request to an API route.
|
||||
|
||||
If you check traces in your exporter, you'll find new traces reported.
|
||||
|
||||
### Trace Span Names
|
||||
|
||||
Trace span names start with the following keywords based on what it's reporting:
|
||||
|
||||
- `{methodName} {URL}` when reporting HTTP requests, where `{methodName}` is the HTTP method, and `{URL}` is the URL the request is sent to.
|
||||
- `route:` when reporting route handlers running on an HTTP requests.
|
||||
- `middleware:` when reporting a middleware running on an HTTP request.
|
||||
- `workflow:` when reporting a workflow execution.
|
||||
- `step:` when reporting a step in a workflow execution.
|
||||
- `query.graph:` when reporting Query usages.
|
||||
- `pg.query:` when reporting database queries and operations.
|
||||
@@ -0,0 +1,159 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Logging`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to use Medusa’s logging utility.
|
||||
|
||||
## Logger Class
|
||||
|
||||
Medusa provides a `Logger` class with advanced logging functionalities. This includes configuring logging levels or saving logs to a file.
|
||||
|
||||
The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`.
|
||||
|
||||
---
|
||||
|
||||
## How to Log a Message
|
||||
|
||||
Resolve the `logger` using the Medusa container to log a message in your resource.
|
||||
|
||||
For example, create the file `src/jobs/log-message.ts` with the following content:
|
||||
|
||||
export const highlights = [
|
||||
["7", "resolve", "Resolve the `Logger` class."],
|
||||
["9", "info", "Log a message of level `info`."]
|
||||
]
|
||||
|
||||
```ts title="src/jobs/log-message.ts" highlights={highlights}
|
||||
import { MedusaContainer } from "@medusajs/framework/types"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||
|
||||
export default async function myCustomJob(
|
||||
container: MedusaContainer
|
||||
) {
|
||||
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
||||
|
||||
logger.info("I'm using the logger!")
|
||||
}
|
||||
|
||||
export const config = {
|
||||
name: "test-logger",
|
||||
// execute every minute
|
||||
schedule: "* * * * *",
|
||||
}
|
||||
```
|
||||
|
||||
This creates a scheduled job that resolves the `logger` from the Medusa container and uses it to log a message.
|
||||
|
||||
### Test the Scheduled Job
|
||||
|
||||
To test out the above scheduled job, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
After a minute, you'll see the following message as part of the logged messages:
|
||||
|
||||
```text
|
||||
info: I'm using the logger!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Log Levels
|
||||
|
||||
The `Logger` class has the following methods:
|
||||
|
||||
- `info`: The message is logged with level `info`.
|
||||
- `warn`: The message is logged with level `warn`.
|
||||
- `error`: The message is logged with level `error`.
|
||||
- `debug`: The message is logged with level `debug`.
|
||||
|
||||
Each of these methods accepts a string parameter to log in the terminal with the associated level.
|
||||
|
||||
---
|
||||
|
||||
## Logging Configurations
|
||||
|
||||
### Log Level
|
||||
|
||||
The available log levels, from lowest to highest levels, are:
|
||||
|
||||
1. `silly` (default, meaning messages of all levels are logged)
|
||||
2. `debug`
|
||||
3. `info`
|
||||
4. `warn`
|
||||
5. `error`
|
||||
|
||||
You can change that by setting the `LOG_LEVEL` environment variable to the minimum level you want to be logged.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
LOG_LEVEL=error
|
||||
```
|
||||
|
||||
This logs `error` messages only.
|
||||
|
||||
<Note title="Important">
|
||||
|
||||
The environment variable must be set as a system environment variable and not in `.env`.
|
||||
|
||||
</Note>
|
||||
|
||||
### Save Logs in a File
|
||||
|
||||
Aside from showing the logs in the terminal, you can save the logs in a file by setting the `LOG_FILE` environment variable to the path of the file relative to the Medusa server’s root directory.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
LOG_FILE=all.log
|
||||
```
|
||||
|
||||
Your logs are now saved in the `all.log` file at the root of your Medusa application.
|
||||
|
||||
<Note title="Important">
|
||||
|
||||
The environment variable must be set as a system environment variable and not in `.env`.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Show Log with Progress
|
||||
|
||||
The `Logger` class has an `activity` method used to log a message of level `info`. If the Medusa application is running in a development environment, a spinner starts to show the activity's progress.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/jobs/log-message.ts"
|
||||
import { MedusaContainer } from "@medusajs/framework/types"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||
|
||||
export default async function myCustomJob(
|
||||
container: MedusaContainer
|
||||
) {
|
||||
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
||||
|
||||
const activityId = logger.activity("First log message")
|
||||
|
||||
logger.progress(activityId, `Second log message`)
|
||||
|
||||
logger.success(activityId, "Last log message")
|
||||
}
|
||||
```
|
||||
|
||||
The `activity` method returns the ID of the started activity. This ID can then be passed to one of the following methods of the `Logger` class:
|
||||
|
||||
- `progress`: Log a message of level `info` that indicates progress within that same activity.
|
||||
- `success`: Log a message of level `info` that indicates that the activity has succeeded. This also ends the associated activity.
|
||||
- `failure`: Log a message of level `error` that indicates that the activity has failed. This also ends the associated activity.
|
||||
|
||||
<Note>
|
||||
|
||||
If you configured the `LOG_LEVEL` environment variable to a level higher than those associated with the above methods, their messages won’t be logged.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,12 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Debugging and Testing`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you’ll learn about the tools Medusa provides for testing and debugging your Medusa application.
|
||||
|
||||
By the end of this chapter, you’ll learn:
|
||||
|
||||
- How to use Medusa's `medusa-test-utils` test to write integration tests.
|
||||
- How to use Medusa’s `Logger` utility to log messages.
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Example: Write Integration Tests for API Routes`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to write integration tests for API routes using the [medusaIntegrationTestRunner utility function](../page.mdx).
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Testing Tools Setup",
|
||||
link: "/debugging-and-testing/testing-tools"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
## Test a GET API Route
|
||||
|
||||
Consider the following API route created at `src/api/custom/route.ts`:
|
||||
|
||||
```ts title="src/api/custom/route.ts"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
){
|
||||
res.json({
|
||||
message: "Hello, World!",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
To write an integration test that tests this API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content:
|
||||
|
||||
export const getHighlights = [
|
||||
["8", "api.get", "Send a GET request to the `/custom` API route."]
|
||||
]
|
||||
|
||||
```ts title="integration-tests/http/custom-routes.spec.ts" highlights={getHighlights}
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ api, getContainer }) => {
|
||||
describe("Custom endpoints", () => {
|
||||
describe("GET /custom", () => {
|
||||
it("returns correct message", async () => {
|
||||
const response = await api.get(
|
||||
`/custom`
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data).toHaveProperty("message")
|
||||
expect(response.data.message).toEqual("Hello, World!")
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You use the `medusaIntegrationTestRunner` to write your tests.
|
||||
|
||||
You add a single test that sends a `GET` request to `/custom` using the `api.get` method. For the test to pass, the response is expected to:
|
||||
|
||||
- Have a code status `200`,
|
||||
- Have a `message` property in the returned data.
|
||||
- Have the value of the `message` property equal to `Hello, World!`.
|
||||
|
||||
### Run Tests
|
||||
|
||||
Run the following command to run your tests:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run test:integration
|
||||
```
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](../../page.mdx#add-test-commands).
|
||||
|
||||
</Note>
|
||||
|
||||
This runs your Medusa application and runs the tests available under the `src/integrations/http` directory.
|
||||
|
||||
---
|
||||
|
||||
## Test a POST API Route
|
||||
|
||||
Suppose you have a `hello` module whose main service extends the service factory, and that has the following model:
|
||||
|
||||
```ts title="src/modules/hello/models/my-custom.ts"
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text(),
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
And consider that the file `src/api/custom/route.ts` defines another route handler for `POST` requests:
|
||||
|
||||
```ts title="src/api/custom/route.ts"
|
||||
// other imports...
|
||||
import HelloModuleService from "../../../modules/hello/service"
|
||||
|
||||
// ...
|
||||
|
||||
export async function POST(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) {
|
||||
const helloModuleService: HelloModuleService = req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
const myCustom = await helloModuleService.createMyCustoms(
|
||||
req.body
|
||||
)
|
||||
|
||||
res.json({
|
||||
my_custom: myCustom,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This API route creates a new record of `MyCustom`.
|
||||
|
||||
To write tests for this API route, add the following at the end of the `testSuite` function in `integration-tests/http/custom-routes.spec.ts`:
|
||||
|
||||
export const postHighlights = [
|
||||
["14", "api.post", "Send a POST request to the `/custom` API route."]
|
||||
]
|
||||
|
||||
```ts title="integration-tests/http/custom-routes.spec.ts" highlights={postHighlights}
|
||||
// other imports...
|
||||
import HelloModuleService from "../../src/modules/hello/service"
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ api, getContainer }) => {
|
||||
describe("Custom endpoints", () => {
|
||||
// other tests...
|
||||
|
||||
describe("POST /custom", () => {
|
||||
const id = "1"
|
||||
|
||||
it("Creates my custom", async () => {
|
||||
|
||||
const response = await api.post(
|
||||
`/custom`,
|
||||
{
|
||||
id,
|
||||
name: "Test",
|
||||
}
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data).toHaveProperty("my_custom")
|
||||
expect(response.data.my_custom).toEqual({
|
||||
id,
|
||||
name: "Test",
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
This adds a test for the `POST /custom` API route. It uses `api.post` to send the POST request. The `api.post` method accepts as a second parameter the data to pass in the request body.
|
||||
|
||||
The test passes if the response has:
|
||||
|
||||
- Status code `200`.
|
||||
- A `my_custom` property in its data.
|
||||
- Its `id` and `name` match the ones provided to the request.
|
||||
|
||||
### Tear Down Created Record
|
||||
|
||||
To ensure consistency in the database for the rest of the tests after the above test is executed, utilize [Jest's setup and teardown hooks](https://jestjs.io/docs/setup-teardown) to delete the created record.
|
||||
|
||||
Use the `getContainer` function passed as a parameter to the `testSuite` function to resolve a service and use it for setup or teardown purposes
|
||||
|
||||
So, add an `afterAll` hook in the `describe` block for `POST /custom`:
|
||||
|
||||
```ts title="integration-tests/http/custom-routes.spec.ts"
|
||||
// other imports...
|
||||
import HelloModuleService from "../../src/modules/hello/service"
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ api, getContainer }) => {
|
||||
describe("Custom endpoints", () => {
|
||||
// other tests...
|
||||
|
||||
describe("POST /custom", () => {
|
||||
// ...
|
||||
afterAll(() => async () => {
|
||||
const helloModuleService: HelloModuleService = getContainer().resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
await helloModuleService.deleteMyCustoms(id)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The `afterAll` hook resolves the `HelloModuleService` and use its `deleteMyCustoms` to delete the record created by the test.
|
||||
|
||||
---
|
||||
|
||||
## Test a DELETE API Route
|
||||
|
||||
Consider a `/custom/:id` API route created at `src/api/custom/[id]/route.ts`:
|
||||
|
||||
```ts title="src/api/custom/[id]/route.ts"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import HelloModuleService from "../../../modules/hello/service"
|
||||
|
||||
export async function DELETE(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) {
|
||||
const helloModuleService: HelloModuleService = req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
await helloModuleService.deleteMyCustoms(req.params.id)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This API route accepts an ID path parameter, and uses the `HelloModuleService` to delete a `MyCustom` record by that ID.
|
||||
|
||||
To add tests for this API route, add the following to `integration-tests/http/custom-routes.spec.ts`:
|
||||
|
||||
export const deleteHighlights = [
|
||||
["21", "api.delete", "Send a DELETE request to the `/custom/:id` API route."]
|
||||
]
|
||||
|
||||
```ts title="integration-tests/http/custom-routes.spec.ts" highlights={deleteHighlights}
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ api, getContainer }) => {
|
||||
describe("Custom endpoints", () => {
|
||||
// ...
|
||||
|
||||
describe("DELETE /custom/:id", () => {
|
||||
const id = "1"
|
||||
|
||||
beforeAll(() => async () => {
|
||||
const helloModuleService: HelloModuleService = getContainer().resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
await helloModuleService.createMyCustoms({
|
||||
id,
|
||||
name: "Test",
|
||||
})
|
||||
})
|
||||
|
||||
it("Deletes my custom", async () => {
|
||||
const response = await api.delete(
|
||||
`/custom/${id}`
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data).toHaveProperty("success")
|
||||
expect(response.data.success).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
This adds a new test for the `DELETE /custom/:id` API route. You use the `beforeAll` hook to create a `MyCustom` record using the `HelloModuleService`.
|
||||
|
||||
In the test, you use the `api.delete` method to send a `DELETE` request to `/custom/:id`. The test passes if the response:
|
||||
|
||||
- Has a `200` status code.
|
||||
- Has a `success` property in its data.
|
||||
- The `success` property's value is true.
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Write Integration Tests`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about the `medusaIntegrationTestRunner` utility function used to write integration tests.
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Testing Tools Setup",
|
||||
link: "/debugging-and-testing/testing-tools"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
## medusaIntegrationTestRunner Utility
|
||||
|
||||
The `medusaIntegrationTestRunner` utility function is provided by the `medusa-test-utils` package to create integration tests in your Medusa project. It runs a full Medusa application, allowing you test API routes, workflows, or other customizations.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["4", "api", "A set of utility methods used to send requests to the Medusa application."],
|
||||
["4", "getContainer", "A function to retrieve the Medusa container."]
|
||||
]
|
||||
|
||||
```ts title="integration-tests/http/test.spec.ts" highlights={highlights}
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ api, getContainer }) => {
|
||||
// TODO write tests...
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`.
|
||||
|
||||
`testSuite`'s value is a function that defines the tests to run. The function accepts as a parameter an object that has the following properties:
|
||||
|
||||
- `api`: a set of utility methods used to send requests to the Medusa application. It has the following methods:
|
||||
- `get`: Send a `GET` request to an API route.
|
||||
- `post`: Send a `POST` request to an API route.
|
||||
- `delete`: Send a `DELETE` request to an API route.
|
||||
- `getContainer`: a function that retrieves the Medusa Container. Use the `getContainer().resolve` method to resolve resources from the Medusa Container.
|
||||
|
||||
The tests in the `testSuite` function are written using [Jest](https://jestjs.io/).
|
||||
|
||||
---
|
||||
|
||||
### Run Tests
|
||||
|
||||
Run the following command to run your tests:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run test:integration
|
||||
```
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](../page.mdx#add-test-commands).
|
||||
|
||||
</Note>
|
||||
|
||||
This runs your Medusa application and runs the tests available under the `src/integrations/http` directory.
|
||||
|
||||
---
|
||||
|
||||
## Other Options and Inputs
|
||||
|
||||
Refer to [this reference in the Development Resources documentation](!resources!/test-tools-reference/medusaIntegrationTestRunner) for other available parameter options and inputs of the `testSuite` function.
|
||||
|
||||
---
|
||||
|
||||
## Database Used in Tests
|
||||
|
||||
The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end.
|
||||
|
||||
To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](!resources!/test-tools-reference/medusaIntegrationTestRunner).
|
||||
|
||||
---
|
||||
|
||||
## Example Integration Tests
|
||||
|
||||
The next chapters provide examples of writing integration tests for API routes and workflows.
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Example: Write Integration Tests for Workflows`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to write integration tests for workflows using the [medusaIntegrationTestRunner utility function](../page.mdx).
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Testing Tools Setup",
|
||||
link: "/debugging-and-testing/testing-tools"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
## Write Integration Test for Workflow
|
||||
|
||||
Consider you have the following workflow defined at `src/workflows/hello-world.ts`:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts"
|
||||
import {
|
||||
createWorkflow,
|
||||
createStep,
|
||||
StepResponse,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep("step-1", () => {
|
||||
return new StepResponse("Hello, World!")
|
||||
})
|
||||
|
||||
export const helloWorldWorkflow = createWorkflow(
|
||||
"hello-world-workflow",
|
||||
() => {
|
||||
const message = step1()
|
||||
|
||||
return new WorkflowResponse(message)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
To write a test for this workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content:
|
||||
|
||||
```ts title="integration-tests/http/workflow.spec.ts"
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import { helloWorldWorkflow } from "../../src/workflows/hello-world"
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ getContainer }) => {
|
||||
describe("Test hello-world workflow", () => {
|
||||
it("returns message", async () => {
|
||||
const { result } = await helloWorldWorkflow(getContainer())
|
||||
.run()
|
||||
|
||||
expect(result).toEqual("Hello, World!")
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`.
|
||||
|
||||
---
|
||||
|
||||
## Run Test
|
||||
|
||||
Run the following command to run your tests:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run test:integration
|
||||
```
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](../../page.mdx#add-test-commands).
|
||||
|
||||
</Note>
|
||||
|
||||
This runs your Medusa application and runs the tests available under the `integrations/http` directory.
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Example: Integration Tests for a Module`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, find an example of writing an integration test for a module using the [moduleIntegrationTestRunner utility function](../page.mdx).
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Testing Tools Setup",
|
||||
link: "/debugging-and-testing/testing-tools"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
## Write Integration Test for Module
|
||||
|
||||
Consider a `hello` module with a `HelloModuleService` that has a `getMessage` method:
|
||||
|
||||
```ts title="src/modules/hello/service.ts"
|
||||
import { MedusaService } from "@medusajs/framework/utils"
|
||||
import MyCustom from "./models/my-custom"
|
||||
|
||||
class HelloModuleService extends MedusaService({
|
||||
MyCustom,
|
||||
}){
|
||||
getMessage(): string {
|
||||
return "Hello, World!"
|
||||
}
|
||||
}
|
||||
|
||||
export default HelloModuleService
|
||||
```
|
||||
|
||||
To create an integration test for the method, create the file `src/modules/hello/__tests__/service.spec.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/__tests__/service.spec.ts"
|
||||
import { moduleIntegrationTestRunner } from "medusa-test-utils"
|
||||
import { HELLO_MODULE } from ".."
|
||||
import HelloModuleService from "../service"
|
||||
import MyCustom from "../models/my-custom"
|
||||
|
||||
moduleIntegrationTestRunner<HelloModuleService>({
|
||||
moduleName: HELLO_MODULE,
|
||||
moduleModels: [MyCustom],
|
||||
resolve: "./src/modules/hello",
|
||||
testSuite: ({ service }) => {
|
||||
describe("HelloModuleService", () => {
|
||||
it("says hello world", () => {
|
||||
const message = service.getMessage()
|
||||
|
||||
expect(message).toEqual("Hello, World!")
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You use the `moduleIntegrationTestRunner` function to add tests for the `hello` module. You have one test that passes if the `getMessage` method returns the `"Hello, World!"` string.
|
||||
|
||||
---
|
||||
|
||||
## Run Test
|
||||
|
||||
Run the following command to run your module integration tests:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run test:modules
|
||||
```
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
If you don't have a `test:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](../../page.mdx#add-test-commands).
|
||||
|
||||
</Note>
|
||||
|
||||
This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory.
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Prerequisites } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Write Tests for Modules`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about the `moduleIntegrationTestRunner` utility function and how to use it to write integration tests for a module's main service.
|
||||
|
||||
<Prerequisites
|
||||
items={[
|
||||
{
|
||||
text: "Testing Tools Setup",
|
||||
link: "/debugging-and-testing/testing-tools"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
## moduleIntegrationTestRunner Utility
|
||||
|
||||
The `moduleIntegrationTestRunner` utility function is provided by the `medusa-test-utils` package to create integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled.
|
||||
|
||||
For example, assuming you have a `hello` module, create a test file at `src/modules/hello/__tests__/service.spec.ts`:
|
||||
|
||||
```ts title="src/modules/hello/__tests__/service.spec.ts"
|
||||
import { moduleIntegrationTestRunner } from "medusa-test-utils"
|
||||
import { HELLO_MODULE } from ".."
|
||||
import HelloModuleService from "../service"
|
||||
import MyCustom from "../models/my-custom"
|
||||
|
||||
moduleIntegrationTestRunner<HelloModuleService>({
|
||||
moduleName: HELLO_MODULE,
|
||||
moduleModels: [MyCustom],
|
||||
resolve: "./src/modules/hello",
|
||||
testSuite: ({ service }) => {
|
||||
// TODO write tests
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties:
|
||||
|
||||
- `moduleName`: The name of the module.
|
||||
- `moduleModels`: An array of models in the module. Refer to [this section](#write-tests-for-modules-without-data-models) if your module doesn't have data models.
|
||||
- `resolve`: The path to the model.
|
||||
- `testSuite`: A function that defines the tests to run.
|
||||
|
||||
The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property.
|
||||
|
||||
</Note>
|
||||
|
||||
The tests in the `testSuite` function are written using [Jest](https://jestjs.io/).
|
||||
|
||||
---
|
||||
|
||||
## Run Tests
|
||||
|
||||
Run the following command to run your module integration tests:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run test:modules
|
||||
```
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
If you don't have a `test:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](../page.mdx#add-test-commands).
|
||||
|
||||
</Note>
|
||||
|
||||
This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory.
|
||||
|
||||
---
|
||||
|
||||
## Pass Module Options
|
||||
|
||||
If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { moduleIntegrationTestRunner } from "medusa-test-utils"
|
||||
import HelloModuleService from "../service"
|
||||
|
||||
moduleIntegrationTestRunner<HelloModuleService>({
|
||||
moduleOptions: {
|
||||
apiKey: "123",
|
||||
},
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Write Tests for Modules without Data Models
|
||||
|
||||
If your module doesn't have a data model, pass a dummy model in the `moduleModels` property.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { moduleIntegrationTestRunner } from "medusa-test-utils"
|
||||
import HelloModuleService from "../service"
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const DummyModel = model.define("dummy_model", {
|
||||
id: model.id().primaryKey(),
|
||||
})
|
||||
|
||||
moduleIntegrationTestRunner<HelloModuleService>({
|
||||
moduleModels: [DummyModel],
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Other Options and Inputs
|
||||
|
||||
Refer to [this reference in the Development Resources documentation](!resources!/test-tools-reference/moduleIntegrationTestRunner) for other available parameter options and inputs of the `testSuite` function.
|
||||
|
||||
---
|
||||
|
||||
## Database Used in Tests
|
||||
|
||||
The `moduleIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end.
|
||||
|
||||
To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](!resources!/test-tools-reference/moduleIntegrationTestRunner).
|
||||
@@ -0,0 +1,103 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Medusa Testing Tools`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about Medusa's testing tools and how to install and configure them.
|
||||
|
||||
## medusa-test-utils Package
|
||||
|
||||
Medusa provides a `medusa-test-utils` package with utility tools to create integration tests for your custom API routes, modules, or other Medusa customizations.
|
||||
|
||||
### Install medusa-test-utils
|
||||
|
||||
To use the `medusa-test-utils` package, install it as a `devDependency`:
|
||||
|
||||
```bash npm2yarn
|
||||
npm install --save-dev medusa-test-utils@rc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Install and Configure Jest
|
||||
|
||||
Writing tests with `medusa-test-utils`'s tools requires installing and configuring Jest in your project.
|
||||
|
||||
{/* TODO remove this note at some point in the future */}
|
||||
|
||||
<Note>
|
||||
|
||||
If your Medusa project was created after September 3rd, Jest is already installed and configured.
|
||||
|
||||
</Note>
|
||||
|
||||
Run the following command to install the required Jest dependencies:
|
||||
|
||||
```bash npm2yarn
|
||||
npm install --save-dev jest @types/jest @swc/jest
|
||||
```
|
||||
|
||||
Then, create the file `jest.config.js` with the following content:
|
||||
|
||||
```js title="jest.config.js"
|
||||
const { loadEnv } = require("@medusajs/framework/utils")
|
||||
loadEnv("test", process.cwd())
|
||||
|
||||
module.exports = {
|
||||
transform: {
|
||||
"^.+\\.[jt]s$": [
|
||||
"@swc/jest",
|
||||
{
|
||||
jsc: {
|
||||
parser: { syntax: "typescript", decorators: true },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: "node",
|
||||
moduleFileExtensions: ["js", "ts", "json"],
|
||||
modulePathIgnorePatterns: ["dist/"],
|
||||
}
|
||||
|
||||
if (process.env.TEST_TYPE === "integration:http") {
|
||||
module.exports.testMatch = ["**/integration-tests/http/*.spec.[jt]s"]
|
||||
} else if (process.env.TEST_TYPE === "integration:modules") {
|
||||
module.exports.testMatch = ["**/src/modules/*/__tests__/**/*.[jt]s"]
|
||||
} else if (process.env.TEST_TYPE === "unit") {
|
||||
module.exports.testMatch = ["**/src/**/__tests__/**/*.unit.spec.[jt]s"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Add Test Commands
|
||||
|
||||
Finally, add the following scripts to `package.json`:
|
||||
|
||||
```json title="package.json"
|
||||
"scripts": {
|
||||
// ...
|
||||
"test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit",
|
||||
"test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit",
|
||||
"test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit"
|
||||
},
|
||||
```
|
||||
|
||||
You now have two commands:
|
||||
|
||||
- `test:integration:http` to run integration tests (for example, for API routes and workflows) available under the `integration-tests/http` directory.
|
||||
- `test:integration:modules` to run integration tests for modules available in any `__tests__` directory under `src/modules`.
|
||||
- `test:unit` to run unit tests in any `__tests__` directory under the `src` directory.
|
||||
|
||||
<Note>
|
||||
|
||||
Medusa provides utility tools for integration tests only. You can write unit tests using Jest.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Test Tools and Writing Tests
|
||||
|
||||
The next chapters explain how to use the testing tools provided by `medusa-test-utils` to write tests.
|
||||
@@ -0,0 +1,90 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Deployment Overview`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about the approach to deploy your Medusa application.
|
||||
|
||||
<Note title="Important">
|
||||
|
||||
As Medusa v2 is still in active development, it's highly recommended not to deploy it for production purposes.
|
||||
|
||||
</Note>
|
||||
|
||||
## Medusa Project Components
|
||||
|
||||
A standard Medusa project is made up of:
|
||||
|
||||
- Medusa application: The Medusa server and the Medusa Admin.
|
||||
- One or more storefronts
|
||||
|
||||

|
||||
|
||||
You can either deploy the Medusa application fully (server with the admin), or deploy the Medusa Admin separately. The storefront is always deployed separately.
|
||||
|
||||
---
|
||||
|
||||
## Deploying the Medusa Server
|
||||
|
||||
You must deploy the Medusa server before the admin or storefront, as both of them connect to it and won’t work without a deployed Medusa server URL.
|
||||
|
||||

|
||||
|
||||
The Medusa server must be deployed to a hosting provider supporting Node.js server deployments, such as Railway, DigitalOcean, AWS, Heroku, etc…
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
For optimal experience, make sure that the hosting provider and plan offer at least 2GB of RAM.
|
||||
|
||||
</Note>
|
||||
|
||||
Your server connects to a PostgreSQL database, Redis, and other services relevant for your setup. Most hosting providers support deploying and managing these databases along with your Medusa server (such as Railway and DigitalOcean).
|
||||
|
||||
Refer to [this reference](!resources!/deployment) to find how-to deployment guides for specific hosting providers.
|
||||
|
||||
---
|
||||
|
||||
## Deploying the Medusa Admin
|
||||
|
||||
### Deploy Admin with the Server
|
||||
|
||||
The Medusa Admin can be deployed on the same hosting with the server.
|
||||
|
||||
In this scenario, make sure the hosting provider and plan of your choice provide at least 2GB of RAM, as the admin build requires high RAM usage.
|
||||
|
||||

|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
The server deployment guides mention details on how to deploy the admin with the Medusa server.
|
||||
|
||||
</Note>
|
||||
|
||||
### Deploy Admin Separately
|
||||
|
||||
You can deploy the admin into a separate hosting provider or instance. The admin can be hosted on providers that support front-end websites and frameworks, such as Vercel.
|
||||
|
||||
<Note title="Important">
|
||||
|
||||
Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the Medusa Admin on the free plan for development purposes, but for commercial projects, you must update your Vercel plan.
|
||||
|
||||
</Note>
|
||||
|
||||
Refer to [this reference](!resources!/deployment) to find how-to deployment guides for specific hosting providers.
|
||||
|
||||
---
|
||||
|
||||
## Deploying the Storefront
|
||||
|
||||
The storefront is deployed separately from the Medusa application, and the hosting options depend on the tools and frameworks you use to create the storefront.
|
||||
|
||||
If you’re using the Next.js Starter storefront, you may deploy the storefront to any hosting provider that supports frontend frameworks, such as Vercel.
|
||||
|
||||
<Note title="Important">
|
||||
|
||||
Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the storefront on the free plan for development purposes, but for commercial projects, you must update your Vercel plan.
|
||||
|
||||
</Note>
|
||||
|
||||
Refer to [this reference](!resources!/deployment) to find how-to deployment guides for specific hosting providers.
|
||||
Loaded 100 of 223 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user