Files
medusa-store/www/apps/resources/app/examples/page.mdx
2025-12-30 09:36:10 +02:00

3941 lines
93 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `Medusa Examples`,
}
# {metadata.title}
This documentation page has examples of customizations useful for your custom development in the Medusa application.
Each section links to the associated documentation page to learn more about it.
## API Routes
An API route is a REST API endpoint that exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems.
### Create API Route
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!",
})
}
```
This creates a `GET` API route at `/hello-world`.
Learn more in [this documentation](!docs!/learn/fundamentals/api-routes).
### Resolve Resources in API Route
To resolve resources from the Medusa container in an API route:
```ts highlights={[["8", "resolve", "Resolve the Product Module's\nmain service from the Medusa container."]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const productModuleService = req.scope.resolve(
Modules.PRODUCT
)
const [, count] = await productModuleService
.listAndCountProducts()
res.json({
count,
})
}
```
This resolves the Product Module's main service.
Learn more in [this documentation](!docs!/learn/fundamentals/medusa-container).
### Use Path Parameters
API routes can accept path parameters.
To do that, 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}!`,
})
}
```
Learn more about path parameters in [this documentation](!docs!/learn/fundamentals/api-routes/parameters#path-parameters).
### Use Query Parameters
API routes can accept query parameters:
export const queryHighlights = [
["11", "req.query.name", "Access the query parameter `name`"],
]
```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}`,
})
}
```
Learn more about query parameters in [this documentation](!docs!/learn/fundamentals/api-routes/parameters#query-parameters).
### Use Body Parameters
API routes can accept request body parameters:
export const bodyHighlights = [
["11", "HelloWorldReq", "Specify the type of the request body parameters."],
["15", "req.body.name", "Access the request body parameter `name`"],
]
```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}!`,
})
}
```
Learn more about request body parameters in [this documentation](!docs!/learn/fundamentals/api-routes/parameters#request-body-parameters).
### Set Response Code
You can change the response code of an API route:
```ts highlights={[["7", "status", "Change the response's status."]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
res.status(200).json({
message: "Hello, World!",
})
}
```
Learn more about setting the response code in [this documentation](!docs!/learn/fundamentals/api-routes/responses#set-response-status-code).
### Execute a Workflow in an API Route
To execute a workflow in an API route:
```ts
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)
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow).
### Change Response Content Type
By default, an API route's response has the content type `application/json`.
To change it to another content type, use the `writeHead` method of `MedusaResponse`:
export const responseContentTypeHighlights = [
["7", "writeHead", "Change the content type in the header."]
]
```ts highlights={responseContentTypeHighlights}
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()
})
}
```
This changes the response type to return an event stream.
Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/responses#change-response-content-type).
### Create Middleware
A middleware is a function executed when a request is sent to an API Route.
Create the file `src/api/middlewares.ts` with the following content:
```ts title="src/api/middlewares.ts"
import type {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
defineMiddlewares,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/custom*",
middlewares: [
(
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => {
console.log("Received a request!")
next()
},
],
},
{
matcher: "/custom/:id",
middlewares: [
(
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => {
console.log("With Path Parameter")
next()
},
],
},
],
})
```
Learn more about middlewares in [this documentation](!docs!/learn/fundamentals/api-routes/middlewares).
### Restrict HTTP Methods in Middleware
To restrict a middleware to an HTTP method:
export const middlewareMethodHighlights = [
["12", "method", "Apply the middleware on `POST` and `PUT` requests only."]
]
```ts title="src/api/middlewares.ts" highlights={middlewareMethodHighlights}
import type {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
defineMiddlewares,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/custom*",
method: ["POST", "PUT"],
middlewares: [
// ...
],
},
],
})
```
### Add Validation for Custom Routes
1. Create a [Zod](https://zod.dev/) schema in the file `src/api/custom/validators.ts`:
```ts title="src/api/custom/validators.ts"
import { z } from "zod"
export const PostStoreCustomSchema = z.object({
a: z.number(),
b: z.number(),
})
```
2. Add a validation middleware to the custom route in `src/api/middlewares.ts`:
```ts title="src/api/middlewares.ts" highlights={[["13", "validateAndTransformBody"]]}
import {
validateAndTransformBody,
defineMiddlewares,
} from "@medusajs/framework/http"
import { PostStoreCustomSchema } from "./custom/validators"
export default defineMiddlewares({
routes: [
{
matcher: "/custom",
method: "POST",
middlewares: [
validateAndTransformBody(PostStoreCustomSchema),
],
},
],
})
```
3. Use the validated body in the `/custom` API route:
```ts title="src/api/custom/route.ts" highlights={[["14", "validatedBody"]]}
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,
})
}
```
Learn more about request body validation in [this documentation](!docs!/learn/fundamentals/api-routes/validation).
### Pass Additional Data to API Route
In this example, you'll pass additional data to the Create Product API route, then consume its hook:
<Note>
Find this example in details in [this documentation](!docs!/learn/customization/extend-features/extend-create-product).
</Note>
1. Create the file `src/api/middlewares.ts` with the following content:
```ts title="src/api/middlewares.ts" highlights={[["10", "brand_id", "Replace with your custom field."]]}
import { defineMiddlewares } from "@medusajs/framework/http"
import { z } from "zod"
export default defineMiddlewares({
routes: [
{
matcher: "/admin/products",
method: ["POST"],
additionalDataValidator: {
brand_id: z.string().optional(),
},
},
],
})
```
<Note>
Learn more about additional data in [this documentation](!docs!/learn/fundamentals/api-routes/additional-data).
</Note>
2. Create the file `src/workflows/hooks/created-product.ts` with the following content:
```ts
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
import { StepResponse } from "@medusajs/framework/workflows-sdk"
createProductsWorkflow.hooks.productsCreated(
(async ({ products, additional_data }, { container }) => {
if (!additional_data.brand_id) {
return new StepResponse([], [])
}
// TODO perform custom action
}),
(async (links, { container }) => {
// TODO undo the action in the compensation
})
)
```
<Note>
Learn more about workflow hooks in [this documentation](!docs!/learn/fundamentals/workflows/workflow-hooks).
</Note>
### Restrict an API Route to Admin Users
You can protect API routes by restricting access to authenticated admin users only.
Add the following middleware in `src/api/middlewares.ts`:
```ts title="src/api/middlewares.ts" highlights={[["11", "authenticate"]]}
import {
defineMiddlewares,
authenticate,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/custom/admin*",
middlewares: [
authenticate(
"user",
["session", "bearer", "api-key"]
),
],
},
],
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes).
### Restrict an API Route to Logged-In Customers
You can protect API routes by restricting access to authenticated customers only.
Add the following middleware in `src/api/middlewares.ts`:
```ts title="src/api/middlewares.ts" highlights={[["11", "authenticate"]]}
import {
defineMiddlewares,
authenticate,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/custom/customer*",
middlewares: [
authenticate("customer", ["session", "bearer"]),
],
},
],
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes).
### Retrieve Logged-In Admin User
To retrieve the currently logged-in user in an API route:
<Note>
Requires setting up the authentication middleware as explained in [this example](#restrict-an-api-route-to-admin-users).
</Note>
```ts highlights={[["16", "req.auth_context.actor_id", "Access the user's ID."]]}
import type {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const userModuleService = req.scope.resolve(
Modules.USER
)
const user = await userModuleService.retrieveUser(
req.auth_context.actor_id
)
// ...
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-admin-users-details).
### Retrieve Logged-In Customer
To retrieve the currently logged-in customer in an API route:
<Note>
Requires setting up the authentication middleware as explained in [this example](#restrict-an-api-route-to-logged-in-customers).
</Note>
```ts highlights={[["18", "req.auth_context.actor_id", "Access the customer's ID."]]}
import type {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
if (req.auth_context?.actor_id) {
// retrieve customer
const customerModuleService = req.scope.resolve(
Modules.CUSTOMER
)
const customer = await customerModuleService.retrieveCustomer(
req.auth_context.actor_id
)
}
// ...
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-customers-details).
### Throw Errors in API Route
To throw errors in an API route, use `MedusaError` from the Medusa Framework:
```ts highlights={[["9", "MedusaError"]]}
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."
)
}
// ...
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/errors).
### Override Error Handler of API Routes
To override the error handler of API routes, create the file `src/api/middlewares.ts` with the following content:
```ts title="src/api/middlewares.ts" highlights={[["10", "errorHandler"]]}
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.",
})
},
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/errors#override-error-handler),
### Setting up CORS for Custom API Routes
By default, Medusa configures CORS for all routes starting with `/admin`, `/store`, and `/auth`.
To configure CORS for routes under other prefixes, create the file `src/api/middlewares.ts` with the following content:
```ts title="src/api/middlewares.ts"
import type {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
defineMiddlewares,
} 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)
},
],
},
],
})
```
### Parse Webhook Body
By default, the Medusa application parses a request's body using JSON.
To parse a webhook's body, create the file `src/api/middlewares.ts` with the following content:
```ts title="src/api/middlewares.ts" highlights={[["9"]]}
import {
defineMiddlewares,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/webhooks/*",
bodyParser: { preserveRawBody: true },
method: ["POST"],
},
],
})
```
To access the raw body data in your route, use the `req.rawBody` property:
```ts title="src/api/webhooks/route.ts"
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
export const POST = (
req: MedusaRequest,
res: MedusaResponse
) => {
console.log(req.rawBody)
}
```
---
## Modules
A module is a package of reusable commerce or architectural functionalities. They handle business logic in a class called a service, and define and manage data models that represent tables in the database.
### Create Module
<Note>
Find this example explained in details in [this documentation](!docs!/learn/fundamentals/modules).
</Note>
1. Create the directory `src/modules/blog`.
2. Create the file `src/modules/blog/models/post.ts` with the following data model:
```ts title="src/modules/blog/models/post.ts"
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
id: model.id().primaryKey(),
title: model.text(),
})
export default Post
```
3. Create the file `src/modules/blog/service.ts` with the following service:
```ts title="src/modules/blog/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
Post,
}){
}
export default BlogModuleService
```
4. Create the file `src/modules/blog/index.ts` that exports the module definition:
```ts title="src/modules/blog/index.ts"
import BlogModuleService from "./service"
import { Module } from "@medusajs/framework/utils"
export const BLOG_MODULE = "blog"
export default Module(BLOG_MODULE, {
service: BlogModuleService,
})
```
5. Add the module to the configurations in `medusa-config.ts`:
```ts title="medusa-config.ts"
module.exports = defineConfig({
projectConfig: {
// ...
},
modules: [
{
resolve: "./modules/blog",
},
],
})
```
6. Generate and run migrations:
```bash
npx medusa db:generate blog
npx medusa db:migrate
```
7. Use the module's main service in an API route:
```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import BlogModuleService from "../../modules/blog/service"
import { BLOG_MODULE } from "../../modules/blog"
export async function GET(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const blogModuleService: BlogModuleService = req.scope.resolve(
BLOG_MODULE
)
const post = await blogModuleService.createPosts({
title: "test",
})
res.json({
post,
})
}
```
### Module with Multiple Services
To add services in your module other than the main one, create them in the `services` directory of the module.
For example, create the file `src/modules/blog/services/category.ts` with the following content:
```ts title="src/modules/blog/services/category.ts"
export class CategoryService {
// TODO add methods
}
```
Then, export the service in the file `src/modules/blog/services/index.ts`:
```ts title="src/modules/blog/services/index.ts"
export * from "./category"
```
Finally, resolve the service in your module's main service or loader:
```ts title="src/modules/blog/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
import { CategoryService } from "./services"
type InjectedDependencies = {
categoryService: CategoryService
}
class BlogModuleService extends MedusaService({
Post,
}){
private categoryService: CategoryService
constructor({ categoryService }: InjectedDependencies) {
super(...arguments)
this.categoryService = categoryService
}
}
export default BlogModuleService
```
Learn more in [this documentation](!docs!/learn/fundamentals/modules/multiple-services).
### Accept Module Options
A module can accept options for configurations and secrets.
To accept options in your module:
1. Pass options to the module in `medusa-config.ts`:
```ts title="medusa-config.ts" highlights={[["6", "options"]]}
module.exports = defineConfig({
// ...
modules: [
{
resolve: "./modules/blog",
options: {
apiKey: true,
},
},
],
})
```
2. Access the options in the module's main service:
```ts title="src/modules/blog/service.ts" highlights={[["14", "options"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
// recommended to define type in another file
type ModuleOptions = {
apiKey?: boolean
}
export default class BlogModuleService extends MedusaService({
Post,
}){
protected options_: ModuleOptions
constructor({}, options?: ModuleOptions) {
super(...arguments)
this.options_ = options || {
apiKey: false,
}
}
// ...
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/modules/options).
### Integrate Third-Party System in Module
An example of integrating a dummy third-party system in a module's service:
```ts title="src/modules/blog/service.ts"
import { Logger } from "@medusajs/framework/types"
import { BLOG_MODULE } from ".."
export type ModuleOptions = {
apiKey: string
}
type InjectedDependencies = {
logger: Logger
}
export class BlogClient {
private options_: ModuleOptions
private logger_: Logger
constructor(
{ logger }: InjectedDependencies,
options: ModuleOptions
) {
this.logger_ = logger
this.options_ = options
}
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)
}`)
}
}
```
Find a longer example of integrating a third-party service in [this documentation](!docs!/learn/customization/integrate-systems/service).
---
## Data Models
A data model represents a table in the database. Medusa provides a data model language to intuitively create data models.
### Create Data Model
To create a data model in a module:
<Note>
This assumes you already have a module. If not, follow [this example](#create-module).
</Note>
1. Create the file `src/modules/blog/models/post.ts` with the following data model:
```ts title="src/modules/blog/models/post.ts"
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
id: model.id().primaryKey(),
title: model.text(),
})
export default Post
```
2. Generate and run migrations:
```bash
npx medusa db:generate blog
npx medusa db:migrate
```
Learn more in [this documentation](!docs!/learn/fundamentals/modules#1-create-data-model).
### Data Model Property Types
A data model can have properties of the following types:
1. ID property:
```ts
const Post = model.define("post", {
id: model.id(),
// ...
})
```
2. Text property:
```ts
const Post = model.define("post", {
title: model.text(),
// ...
})
```
3. Number property:
```ts
const Post = model.define("post", {
views: model.number(),
// ...
})
```
4. Big Number property:
```ts
const Post = model.define("post", {
price: model.bigNumber(),
// ...
})
```
5. Boolean property:
```ts
const Post = model.define("post", {
isPublished: model.boolean(),
// ...
})
```
6. Enum property:
```ts
const Post = model.define("post", {
status: model.enum(["draft", "published"]),
// ...
})
```
7. Date-Time property:
```ts
const Post = model.define("post", {
publishedAt: model.dateTime(),
// ...
})
```
8. JSON property:
```ts
const Post = model.define("post", {
metadata: model.json(),
// ...
})
```
9. Array property:
```ts
const Post = model.define("post", {
tags: model.array(),
// ...
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties).
### Set Primary Key
To set an `id` property as the primary key of a data model:
```ts highlights={[["4", "primaryKey"]]}
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
id: model.id().primaryKey(),
// ...
})
export default Post
```
To set a `text` property as the primary key:
```ts highlights={[["4", "primaryKey"]]}
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
title: model.text().primaryKey(),
// ...
})
export default Post
```
To set a `number` property as the primary key:
```ts highlights={[["4", "primaryKey"]]}
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
views: model.number().primaryKey(),
// ...
})
export default Post
```
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#set-primary-key-property).
### Default Property Value
To set the default value of a property:
```ts highlights={[["6"], ["9"]]}
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
status: model
.enum(["draft", "published"])
.default("draft"),
views: model
.number()
.default(0),
// ...
})
export default Post
```
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#property-default-value).
### Nullable Property
To allow `null` values for a property:
```ts highlights={[["4", "nullable"]]}
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
price: model.bigNumber().nullable(),
// ...
})
export default Post
```
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#make-property-optional).
### Unique Property
To create a unique index on a property:
```ts highlights={[["4", "unique"]]}
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
title: model.text().unique(),
// ...
})
export default Post
```
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#unique-property).
### Define Database Index on Property
To define a database index on a property:
```ts highlights={[["5", "index"]]}
import { model } from "@medusajs/framework/utils"
const MyCustom = model.define("my_custom", {
id: model.id().primaryKey(),
title: model.text().index(
"IDX_POST_TITLE"
),
})
export default MyCustom
```
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#define-database-index-on-property).
### Define Composite Index on Data Model
To define a composite index on a data model:
```ts highlights={[["7", "indexes"]]}
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
```
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/index).
### Make a Property Searchable
To make a property searchable using terms or keywords:
```ts highlights={[["4", "searchable"]]}
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
title: model.text().searchable(),
// ...
})
export default Post
```
Then, to search by that property, pass the `q` filter to the `list` or `listAndCount` generated methods of the module's main service:
<Note>
`blogModuleService` is the main service that manages the `Post` data model.
</Note>
```ts
const posts = await blogModuleService.listPosts({
q: "John",
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/properties#searchable-property).
### Create One-to-One Relationship
The following creates a one-to-one relationship between the `User` and `Email` data models:
```ts highlights={[["5", "hasOne"], ["12", "belongsTo"]]}
import { model } from "@medusajs/framework/utils"
const User = model.define("user", {
id: model.id().primaryKey(),
email: model.hasOne(() => Email, {
mappedBy: "user",
}),
})
const Email = model.define("email", {
id: model.id().primaryKey(),
user: model.belongsTo(() => User, {
mappedBy: "email",
}),
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#one-to-one-relationship).
### Create One-to-Many Relationship
The following creates a one-to-many relationship between the `Store` and `Product` data models:
```ts highlights={[["5", "hasMany"], ["12", "belongsTo"]]}
import { model } from "@medusajs/framework/utils"
const Store = model.define("store", {
id: model.id().primaryKey(),
products: model.hasMany(() => Product, {
mappedBy: "store",
}),
})
const Product = model.define("product", {
id: model.id().primaryKey(),
store: model.belongsTo(() => Store, {
mappedBy: "products",
}),
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#one-to-many-relationship).
### Create Many-to-Many Relationship
The following creates a many-to-many relationship between the `Order` and `Product` data models:
```ts highlights={[["5", "manyToMany"], ["12", "manyToMany"]]}
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",
}),
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#many-to-many-relationship).
### Configure Cascades of Data Model
To configure cascade on a data model:
```ts highlights={[["10", "cascades"]]}
import { model } from "@medusajs/framework/utils"
import Product from "./product"
const Store = model.define("store", {
id: model.id().primaryKey(),
products: model.hasMany(() => Product, {
mappedBy: "store",
}),
})
.cascades({
delete: ["products"],
})
```
This configures the delete cascade on the `Store` data model so that, when a store is delete, its products are also deleted.
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#cascades).
### Manage One-to-One Relationship
Consider you have a one-to-one relationship between `Email` and `User` data models, where an email belongs to a user.
To set the ID of the user that an email belongs to:
<Note>
`blogModuleService` is the main service that manages the `Email` and `User` data models.
</Note>
```ts
// when creating an email
const email = await blogModuleService.createEmails({
// other properties...
user: "123",
})
// when updating an email
const email = await blogModuleService.updateEmails({
id: "321",
// other properties...
user: "123",
})
```
And to set the ID of a user's email when creating or updating it:
```ts
// when creating a user
const user = await blogModuleService.createUsers({
// other properties...
email: "123",
})
// when updating a user
const user = await blogModuleService.updateUsers({
id: "321",
// other properties...
email: "123",
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#manage-one-to-one-relationship).
### Manage One-to-Many Relationship
Consider you have a one-to-many relationship between `Product` and `Store` data models, where a store has many products.
To set the ID of the store that a product belongs to:
<Note>
`blogModuleService` is the main service that manages the `Product` and `Store` data models.
</Note>
```ts
// when creating a product
const product = await blogModuleService.createProducts({
// other properties...
store_id: "123",
})
// when updating a product
const product = await blogModuleService.updateProducts({
id: "321",
// other properties...
store_id: "123",
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#manage-one-to-many-relationship)
### Manage Many-to-Many Relationship
Consider you have a many-to-many relationship between `Order` and `Product` data models.
To set the orders a product has when creating it:
<Note>
`blogModuleService` is the main service that manages the `Product` and `Order` data models.
</Note>
```ts
const product = await blogModuleService.createProducts({
// other properties...
orders: ["123", "321"],
})
```
To add new orders to a product without removing the previous associations:
```ts
const product = await blogModuleService.retrieveProduct(
"123",
{
relations: ["orders"],
}
)
const updatedProduct = await blogModuleService.updateProducts({
id: product.id,
// other properties...
orders: [
...product.orders.map((order) => order.id),
"321",
],
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#manage-many-to-many-relationship).
### Retrieve Related Records
To retrieve records related to a data model's records through a relation, pass the `relations` field to the `list`, `listAndCount`, or `retrieve` generated methods:
<Note>
`blogModuleService` is the main service that manages the `Product` and `Order` data models.
</Note>
```ts highlights={[["4", "relations"]]}
const product = await blogModuleService.retrieveProducts(
"123",
{
relations: ["orders"],
}
)
```
Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#retrieve-records-of-relation).
---
## Services
A service is the main resource in a module. It manages the records of your custom data models in the database, or integrate third-party systems.
### Extend Service Factory
The service factory `MedusaService` generates data-management methods for your data models.
To extend the service factory in your module's service:
```ts highlights={[["4", "MedusaService"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
Post,
}){
// TODO implement custom methods
}
export default BlogModuleService
```
The `BlogModuleService` will now have data-management methods for `Post`.
Refer to [this reference](../service-factory-reference/page.mdx) for details on the generated methods.
Learn more about the service factory in [this documentation](!docs!/learn/fundamentals/modules/service-factory).
### Resolve Resources in the Service
To resolve resources from the module's container in a service:
<CodeTabs group="service-type">
<CodeTab label="With Service Factory" value="service-factory">
```ts highlights={[["14"]]}
import { Logger } from "@medusajs/framework/types"
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
type InjectedDependencies = {
logger: Logger
}
class BlogModuleService extends MedusaService({
Post,
}){
protected logger_: Logger
constructor({ logger }: InjectedDependencies) {
super(...arguments)
this.logger_ = logger
this.logger_.info("[BlogModuleService]: Hello World!")
}
// ...
}
export default BlogModuleService
```
</CodeTab>
<CodeTab label="Without Service Factory" value="no-service-factory">
```ts highlights={[["10"]]}
import { Logger } from "@medusajs/framework/types"
type InjectedDependencies = {
logger: Logger
}
export default class BlogModuleService {
protected logger_: Logger
constructor({ logger }: InjectedDependencies) {
this.logger_ = logger
this.logger_.info("[BlogModuleService]: Hello World!")
}
// ...
}
```
</CodeTab>
</CodeTabs>
Learn more in [this documentation](!docs!/learn/fundamentals/modules/container).
### Access Module Options in Service
To access options passed to a module in its service:
```ts highlights={[["14", "options"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
// recommended to define type in another file
type ModuleOptions = {
apiKey?: boolean
}
export default class BlogModuleService extends MedusaService({
Post,
}){
protected options_: ModuleOptions
constructor({}, options?: ModuleOptions) {
super(...arguments)
this.options_ = options || {
apiKey: "",
}
}
// ...
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/modules/options).
### Run Database Query in Service
To run database query in your service:
```ts highlights={[["14", "count"], ["21", "execute"]]}
// other imports...
import {
InjectManager,
MedusaContext,
} from "@medusajs/framework/utils"
class BlogModuleService {
// ...
@InjectManager()
async getCount(
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<number> {
return await sharedContext.manager.count("post")
}
@InjectManager()
async getCountSql(
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<number> {
const data = await sharedContext.manager.execute(
"SELECT COUNT(*) as num FROM post"
)
return parseInt(data[0].num)
}
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/modules/db-operations#run-queries)
### Execute Database Operations in Transactions
To execute database operations within a transaction in your service:
```ts
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@medusajs/framework/mikro-orm/knex"
class BlogModuleService {
// ...
@InjectTransactionManager()
protected async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
const transactionManager = sharedContext.transactionManager
await transactionManager.nativeUpdate(
"post",
{
id: input.id,
},
{
name: input.name,
}
)
// retrieve again
const updatedRecord = await transactionManager.execute(
`SELECT * FROM post WHERE id = '${input.id}'`
)
return updatedRecord
}
@InjectManager()
async update(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
) {
return await this.update_(input, sharedContext)
}
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/modules/db-operations#execute-operations-in-transactions).
---
## Module Links
A module link forms an association between two data models of different modules, while maintaining module isolation.
### Define a Link
To define a link between your custom module and a Commerce Module, such as the Product Module:
1. Create the file `src/links/blog-product.ts` with the following content:
```ts title="src/links/blog-product.ts"
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"
export default defineLink(
ProductModule.linkable.product,
BlogModule.linkable.post
)
```
2. Run the following command to sync the links:
```bash
npx medusa db:migrate
```
Learn more in [this documentation](!docs!/learn/fundamentals/module-links).
### Define a List Link
To define a list link, where multiple records of a model can be linked to a record in another:
```ts highlights={[["9", "isList"]]}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"
export default defineLink(
ProductModule.linkable.product,
{
linkable: BlogModule.linkable.post,
isList: true,
}
)
```
Learn more about list links in [this documentation](!docs!/learn/fundamentals/module-links#define-a-list-link).
### Set Delete Cascade on Link Definition
To ensure a model's records linked to another model are deleted when the linked model is deleted:
```ts highlights={[["9", "deleteCascades"]]}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"
export default defineLink(
ProductModule.linkable.product,
{
linkable: BlogModule.linkable.post,
deleteCascades: true,
}
)
```
Learn more in [this documentation](!docs!/learn/fundamentals/module-links#define-a-list-link).
### Add Custom Columns to Module Link
To add a custom column to the table that stores the linked records of two data models:
```ts highlights={[["9", "database"]]}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"
export default defineLink(
ProductModule.linkable.product,
BlogModule.linkable.post,
{
database: {
extraColumns: {
metadata: {
type: "json",
},
},
},
}
)
```
Then, to set the custom column when creating or updating a link between records:
```ts
await link.create({
[Modules.PRODUCT]: {
product_id: "123",
},
HELLO_MODULE: {
my_custom_id: "321",
},
data: {
metadata: {
test: true,
},
},
})
```
To retrieve the custom column when retrieving linked records using Query:
```ts
import productBlogLink from "../links/product-blog"
// ...
const { data } = await query.graph({
entity: productBlogLink.entryPoint,
fields: ["metadata", "product.*", "post.*"],
filters: {
product_id: "prod_123",
},
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/module-links/custom-columns).
### Create Link Between Records
To create a link between two records using Link:
```ts
import { Modules } from "@medusajs/framework/utils"
import { BLOG_MODULE } from "../../modules/blog"
// ...
await link.create({
[Modules.PRODUCT]: {
product_id: "prod_123",
},
[HELLO_MODULE]: {
my_custom_id: "mc_123",
},
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/module-links/link#create-link).
### Dismiss Link Between Records
To dismiss links between records using Link:
```ts
import { Modules } from "@medusajs/framework/utils"
import { BLOG_MODULE } from "../../modules/blog"
// ...
await link.dismiss({
[Modules.PRODUCT]: {
product_id: "prod_123",
},
[BLOG_MODULE]: {
post_id: "mc_123",
},
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/module-links/link#dismiss-link).
### Cascade Delete Linked Records
To cascade delete records linked to a deleted record:
```ts
import { Modules } from "@medusajs/framework/utils"
// ...
await productModuleService.deleteVariants([variant.id])
await link.delete({
[Modules.PRODUCT]: {
product_id: "prod_123",
},
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/module-links/link#cascade-delete-linked-records).
### Restore Linked Records
To restore records that were soft-deleted because they were linked to a soft-deleted record:
```ts
import { Modules } from "@medusajs/framework/utils"
// ...
await productModuleService.restoreProducts(["prod_123"])
await link.restore({
[Modules.PRODUCT]: {
product_id: "prod_123",
},
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/module-links/link#restore-linked-records).
---
## Query
Query fetches data across modules. Its a set of methods registered in the Medusa container under the `query` key.
### Retrieve Records of Data Model
To retrieve records using Query in an API route:
```ts highlights={[["15", "graph"]]}
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 })
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query).
### Retrieve Linked Records of Data Model
To retrieve records linked to a data model:
```ts highlights={[["20"]]}
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",
"product.*",
],
})
res.json({ my_customs: myCustoms })
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query#retrieve-linked-records).
### Apply Filters to Retrieved Records
To filter the retrieved records:
```ts highlights={[["18", "filters"]]}
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"],
filters: {
id: [
"mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
"mc_01HWSVWK3KYHKQEE6QGS2JC3FX",
],
},
})
res.json({ my_customs: myCustoms })
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query#apply-filters).
### Apply Pagination and Sort Records
To paginate and sort retrieved records:
```ts highlights={[["21", "pagination"]]}
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,
metadata: { count, take, skip } = {},
} = await query.graph({
entity: "my_custom",
fields: ["id", "name"],
pagination: {
skip: 0,
take: 10,
order: {
name: "DESC",
},
},
})
res.json({
my_customs: myCustoms,
count,
take,
skip,
})
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query#sort-records).
---
## Workflows
A workflow is a series of queries and actions that complete a task.
A workflow allows you to track its execution's progress, provide roll-back logic for each step to mitigate data inconsistency when errors occur, automatically retry failing steps, and more.
### Create a Workflow
To create a workflow:
1. Create the first step at `src/workflows/hello-world/steps/step-1.ts` with the following content:
```ts title="src/workflows/hello-world/steps/step-1.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
export const step1 = createStep("step-1", async () => {
return new StepResponse(`Hello from step one!`)
})
```
2. Create the second step at `src/workflows/hello-world/steps/step-2.ts` with the following content:
```ts title="src/workflows/hello-world/steps/step-2.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
type StepInput = {
name: string
}
export const step2 = createStep(
"step-2",
async ({ name }: StepInput) => {
return new StepResponse(`Hello ${name} from step two!`)
}
)
```
3. Create the workflow at `src/workflows/hello-world/index.ts` with the following content:
```ts title="src/workflows/hello-world/index.ts"
import {
createWorkflow,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { step1 } from "./steps/step-1"
import { step2 } from "./steps/step-2"
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
```
Learn more in [this documentation](!docs!/learn/fundamentals/workflows).
### Execute a Workflow
<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>
Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow).
### Step with a Compensation Function
Pass a compensation function that undoes what a step did as a second parameter to `createStep`:
```ts highlights={[["15"]]}
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...")
}
)
```
Learn more in [this documentation](!docs!/learn/fundamentals/workflows/compensation-function).
### Manipulate Variables in Workflow
To manipulate variables within a workflow's constructor function, use `transform` from the Workflows SDK:
```ts highlights={[["14", "transform"]]}
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)
}
)
```
Learn more in [this documentation](!docs!/learn/fundamentals/workflows/variable-manipulation)
### Using Conditions in Workflow
To perform steps or set a variable's value based on a condition, use `when-then` from the Workflows SDK:
```ts highlights={[["14", "when"]]}
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(() => {
return isActiveStep()
})
// executed without condition
const anotherStepResult = anotherStep(result)
return new WorkflowResponse(
anotherStepResult
)
}
)
```
### Run Workflow in Another
To run a workflow in another, use the workflow's `runAsStep` special method:
```ts highlights={[["11", "runAsStep"]]}
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: [
// ...
],
},
})
// ...
}
)
```
Learn more in [this documentation](!docs!/learn/fundamentals/workflows/execute-another-workflow).
### Consume a Workflow Hook
To consume a workflow hook, create a file under `src/workflows/hooks`:
```ts title="src/workflows/hooks/product-created.ts"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
createProductsWorkflow.hooks.productsCreated(
async ({ products, additional_data }, { container }) => {
// TODO perform an action
},
async (dataFromStep, { container }) => {
// undo the performed action
}
)
```
This executes a custom step at the hook's designated point in the workflow.
Learn more in [this documentation](!docs!/learn/fundamentals/workflows/workflow-hooks).
### Expose a Hook
To expose a hook in a workflow, pass it in the second parameter of the returned `WorkflowResponse`:
```ts highlights={[["19", "hooks"]]}
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],
})
}
)
```
Learn more in [this documentation](!docs!/learn/fundamentals/workflows/add-workflow-hook).
### Retry Steps
To configure steps to retry in case of errors, pass the `maxRetries` step option:
```ts highlights={[["10"]]}
import {
createStep,
} from "@medusajs/framework/workflows-sdk"
export const step1 = createStep(
{
name: "step-1",
maxRetries: 2,
},
async () => {
console.log("Executing step 1")
throw new Error("Oops! Something happened.")
}
)
```
Learn more in [this documentation](!docs!/learn/fundamentals/workflows/retry-failed-steps).
### Run Steps in Parallel
If steps in a workflow don't depend on one another, run them in parallel using `parallel` from the Workflows SDK:
```ts highlights={[["22", "parallelize"]]}
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)
}
)
```
Learn more in [this documentation](!docs!/learn/fundamentals/workflows/parallel-steps).
### Configure Workflow Timeout
To configure the timeout of a workflow, at which the workflow's status is changed, but its execution isn't stopped, use the `timeout` configuration:
```ts highlights={[["10"]]}
import {
createStep,
createWorkflow,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
// step import...
const myWorkflow = createWorkflow({
name: "hello-world",
timeout: 2, // 2 seconds
}, function () {
const str1 = step1()
return new WorkflowResponse({
message: str1,
})
})
export default myWorkflow
```
Learn more in [this documentation](!docs!/learn/fundamentals/workflows/workflow-timeout).
### Configure Step Timeout
To configure a step's timeout, at which its state changes but its execution isn't stopped, use the `timeout` property:
```ts highlights={[["4"]]}
const step1 = createStep(
{
name: "step-1",
timeout: 2, // 2 seconds
},
async () => {
// ...
}
)
```
Learn more in [this documentation](!docs!/learn/fundamentals/workflows/workflow-timeout#configure-step-timeout).
### Long-Running Workflow
A long-running workflow is a workflow that runs in the background. You can wait before executing some of its steps until another external or separate action occurs.
To create a long-running workflow, configure any of its steps to be `async` without returning any data:
```ts highlights={[["4"]]}
const step2 = createStep(
{
name: "step-2",
async: true,
},
async () => {
console.log("Waiting to be successful...")
}
)
```
Learn more in [this documentation](!docs!/learn/fundamentals/workflows/long-running-workflow).
### Change Step Status in Long-Running Workflow
To change a step's status:
1. Grab the workflow's transaction ID when you run it:
```ts
const { transaction } = await myLongRunningWorkflow(req.scope)
.run()
```
2. In an API route, workflow, or other resource, change a step's status to successful using the [Workflow Engine Module](../infrastructure-modules/workflow-engine/page.mdx):
export const stepSuccessHighlights = [
["5", "setStepSuccess", "Change a step's status to success"],
["8", "transactionId", "Pass the workflow's transaction ID"],
["9", "stepId", "The ID of the step to change its status."],
["10", "workflowId", "The ID of the workflow that the step belongs to."]
]
```ts highlights={stepSuccessHighlights}
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,
},
})
```
3. In an API route, workflow, or other resource, change a step's status to failure using the [Worfklow Engine Module](../infrastructure-modules/workflow-engine/page.mdx):
export const stepFailureHighlights = [
["5", "setStepFailure", "Change a step's status to failure"],
["8", "transactionId", "Pass the workflow's transaction ID"],
["9", "stepId", "The ID of the step to change its status."],
["10", "workflowId", "The ID of the workflow that the step belongs to."]
]
```ts highlights={stepFailureHighlights}
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,
},
})
```
Learn more in [this documentation](!docs!/learn/fundamentals/workflows/long-running-workflow).
### Access Long-Running Workflow's Result
Use the Workflow Engine Module's `subscribe` and `unsubscribe` methods to access the status of a long-running workflow.
For example, in an API route:
```ts highlights={[["18", "subscribe", "Subscribe to the workflow's status changes."]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import myWorkflow from "../../../workflows/hello-world"
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(
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)
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result).
---
## Subscribers
A subscriber is a function executed whenever the event it listens to is emitted.
### Create a Subscriber
To create a subscriber that listens to the `product.created` event, create the file `src/subscribers/product-created.ts` with the following content:
```ts title="src/subscribers/product-created.ts"
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",
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/events-and-subscribers).
### Resolve Resources in Subscriber
To resolve resources from the Medusa container in a subscriber, use the `container` property of its parameter:
```ts highlights={[["6", "container"], ["8", "resolve", "Resolve the Product Module's main service."]]}
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
export default async function productCreateHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const productModuleService = 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`,
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/events-and-subscribers#resolve-resources).
### Send a Notification to Reset Password
To send a notification, such as an email when a user requests to reset their password, create a subscriber at `src/subscribers/handle-reset.ts` with the following content:
```ts title="src/subscribers/handle-reset.ts"
import {
SubscriberArgs,
type SubscriberConfig,
} from "@medusajs/medusa"
import { Modules } from "@medusajs/framework/utils"
export default async function resetPasswordTokenHandler({
event: { data: {
entity_id: email,
token,
actor_type,
} },
container,
}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) {
const notificationModuleService = container.resolve(
Modules.NOTIFICATION
)
const urlPrefix = actor_type === "customer" ?
"https://storefront.com" :
"https://admin.com"
await notificationModuleService.createNotifications({
to: email,
channel: "email",
template: "reset-password-template",
data: {
// a URL to a frontend application
url: `${urlPrefix}/reset-password?token=${token}&email=${email}`,
},
})
}
export const config: SubscriberConfig = {
event: "auth.password_reset",
}
```
Learn more in [this documentation](../commerce-modules/auth/reset-password/page.mdx).
### Execute a Workflow in a Subscriber
To execute a workflow in a subscriber:
```ts
import {
type SubscriberConfig,
type SubscriberArgs,
} from "@medusajs/framework"
import myWorkflow from "../workflows/hello-world"
import { Modules } from "@medusajs/framework/utils"
export default async function handleCustomerCreate({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const userId = data.id
const userModuleService = 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",
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow)
---
## Scheduled Jobs
A scheduled job is a function executed at a specified interval of time in the background of your Medusa application.
### Create a Scheduled Job
To create a scheduled job, 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: "* * * * *",
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/scheduled-jobs).
### Resolve Resources in Scheduled Job
To resolve resources in a scheduled job, use the `container` accepted as a first parameter:
```ts highlights={[["5", "container"], ["7", "resolve", "Resolve the Product Module's main service."]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
export default async function myCustomJob(
container: MedusaContainer
) {
const productModuleService = 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: "* * * * *",
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/scheduled-jobs#resolve-resources)
### Specify a Job's Execution Number
To limit the scheduled job's execution to a number of times during the Medusa application's runtime, use the `numberOfExecutions` configuration:
```ts highlights={[["9", "numberOfExecutions"]]}
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,
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/scheduled-jobs/execution-number).
### Execute a Workflow in a Scheduled Job
To execute a workflow in a scheduled job:
```ts
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 * * *`,
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow)
---
## Loaders
A loader is a function defined in a module that's executed when the Medusa application starts.
### Create a Loader
To create a loader, add it to 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!"
)
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/modules/loaders).
### Resolve Resources in Loader
To resolve resources in a loader, use the `container` property of its first parameter:
```ts highlights={[["9", "container"], ["11", "resolve", "Resolve the Logger from the module's container."]]}
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!")
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/modules/container).
### Access Module Options
To access a module's options in its loader, use the `options` property of its first parameter:
```ts highlights={[["11", "options"]]}
import {
LoaderOptions,
} from "@medusajs/framework/types"
// recommended to define type in another file
type ModuleOptions = {
apiKey?: boolean
}
export default async function helloWorldLoader({
options,
}: LoaderOptions<ModuleOptions>) {
console.log(
"[HELLO MODULE] Just started the Medusa application!",
options
)
}
```
Learn more in [this documentation](!docs!/learn/fundamentals/modules/options).
### Register Resources in the Module's Container
To register a resource in the Module's container using a loader, use the `container`'s `registerAdd` method:
```ts highlights={[["9", "registerAdd"]]}
import {
LoaderOptions,
} from "@medusajs/framework/types"
import { asValue } from "@medusajs/framework/awilix"
export default async function helloWorldLoader({
container,
}: LoaderOptions) {
container.registerAdd(
"custom_data",
asValue({
test: true,
})
)
}
```
Where the first parameter of `registerAdd` is the name to register the resource under, and the second parameter is the resource to register.
---
## Admin Customizations
You can customize the Medusa Admin to inject widgets in existing pages, or create new pages using UI routes.
<Note>
For a list of components to use in the admin dashboard, refer to [this documentation](../admin-components/page.mdx).
</Note>
### Create Widget
A widget is a React component that can be injected into an existing page in the admin dashboard.
To create a widget in the admin dashboard, create the file `src/admin/widgets/products-widget.tsx` with the following content:
```tsx title="src/admin/widgets/products-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.list.before",
})
export default ProductWidget
```
Learn more about widgets in [this documentation](!docs!/learn/fundamentals/admin/widgets).
### Receive Details Props in Widgets
Widgets created in a details page, such as widgets in the `product.details.before` injection zone, receive a prop of the data of the details page (for example, the product):
```tsx highlights={[["10", "data"]]}
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
```
Learn more in [this documentation](!docs!/learn/fundamentals/admin/widgets#detail-widget-props).
### Create 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.
To create a UI route in the admin dashboard, create the file `src/admin/routes/custom/page.tsx` with the following content:
```tsx title="src/admin/routes/custom/page.tsx"
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
```
This adds a new page at `localhost:9000/app/custom`.
Learn more in [this documentation](!docs!/learn/fundamentals/admin/ui-routes).
### Create Settings Page
To create a settings page, create a UI route under the `src/admin/routes/settings` directory.
For example, create the file `src/admin/routes/settings/custom/page.tsx` with the following content:
```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 setting page at `localhost:9000/app/settings/custom`.
Learn more in [this documentation](!docs!/learn/fundamentals/admin/ui-routes#create-settings-page)
### Accept Path Parameters in UI Routes
To accept a path parameter in a UI route, name one of the directories in its path in 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"
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
```
This creates a UI route at `localhost:9000/app/custom/:id`, where `:id` is a path parameter.
Learn more in [this documentation](!docs!/learn/fundamentals/admin/ui-routes#path-parameters)
### Send Request to API Route
To send a request to custom API routes from the admin dashboard, use the Fetch API.
For example:
```tsx
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
```
Learn more in [this documentation](!docs!/learn/fundamentals/admin/tips#send-requests-to-api-routes)
### Add Link to Another Page
To add a link to another page in a UI route or a widget, use `react-router-dom`'s `Link` component:
```tsx
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
```
Learn more in [this documentation](!docs!/learn/fundamentals/admin/tips#routing-functionalities).
---
## Integration Tests
Medusa provides a `@medusajs/test-utils` package with utility tools to create integration tests for your custom API routes, modules, or other Medusa customizations.
<Note>
For details on setting up your project for integration tests, refer to [this documentation](!docs!/learn/debugging-and-testing/testing-tools).
</Note>
### Test Custom API Route
To create a test for a custom API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content:
```ts title="integration-tests/http/custom-routes.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/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!")
})
})
})
},
})
```
Then, run the test with the following command:
```bash npm2yarn
npm run test:integration
```
Learn more in [this documentation](!docs!/learn/debugging-and-testing/testing-tools/integration-tests/api-routes).
### Test Workflow
To create a test for a 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 "@medusajs/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!")
})
})
},
})
```
Then, run the test with the following command:
```bash npm2yarn
npm run test:integration
```
Learn more in [this documentation](!docs!/learn/debugging-and-testing/testing-tools/integration-tests/workflows).
### Test Module's Service
To create a test for a module's service, create the test under the `__tests__` directory of the module.
For example, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content:
```ts title="src/modules/blog/__tests__/service.spec.ts"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { BLOG_MODULE } from ".."
import BlogModuleService from "../service"
import Post from "../models/post"
moduleIntegrationTestRunner<BlogModuleService>({
moduleName: BLOG_MODULE,
moduleModels: [Post],
resolve: "./modules/blog",
testSuite: ({ service }) => {
describe("BlogModuleService", () => {
it("says hello world", () => {
const message = service.getMessage()
expect(message).toEqual("Hello, World!")
})
})
},
})
```
Then, run the test with the following command:
```bash npm2yarn
npm run test:modules
```
---
## Commerce Modules
Medusa provides all its commerce features as separate Commerce Modules, such as the Product or Order modules.
<Note>
Refer to the [Commerce Modules](../commerce-modules/page.mdx) documentation for concepts and reference of every module's main service.
</Note>
### Create an Actor Type to Authenticate
To create an actor type that can authenticate to the Medusa application, such as a `manager`:
1. Create the data model in a module:
```ts
import { model } from "@medusajs/framework/utils"
const Manager = model.define("manager", {
id: model.id().primaryKey(),
firstName: model.text(),
lastName: model.text(),
email: model.text(),
})
export default Manager
```
2. Use the `setAuthAppMetadataStep` as a step in a workflow that creates a manager:
```ts
import {
createWorkflow,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
setAuthAppMetadataStep,
} from "@medusajs/medusa/core-flows"
// other imports...
const createManagerWorkflow = createWorkflow(
"create-manager",
function (input: CreateManagerWorkflowInput) {
const manager = createManagerStep({
manager: input.manager,
})
setAuthAppMetadataStep({
authIdentityId: input.authIdentityId,
actorType: "manager",
value: manager.id,
})
return new WorkflowResponse(manager)
}
)
```
3. Use the workflow in an API route that creates a user (manager) of the actor type:
```ts
import type {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import createManagerWorkflow from "../../workflows/create-manager"
type RequestBody = {
first_name: string
last_name: string
email: string
}
export async function POST(
req: AuthenticatedMedusaRequest<RequestBody>,
res: MedusaResponse
) {
// If `actor_id` is present, the request carries
// authentication for an existing manager
if (req.auth_context.actor_id) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Request already authenticated as a manager."
)
}
const { result } = await createManagerWorkflow(req.scope)
.run({
input: {
manager: req.body,
authIdentityId: req.auth_context.auth_identity_id,
},
})
res.status(200).json({ manager: result })
}
```
4. Apply the `authenticate` middleware on the new route in `src/api/middlewares.ts`:
```ts title="src/api/middlewares.ts"
import {
defineMiddlewares,
authenticate,
} from "@medusajs/framework/http"
export default defineMiddlewares({
routes: [
{
matcher: "/manager",
method: "POST",
middlewares: [
authenticate("manager", ["session", "bearer"], {
allowUnregistered: true,
}),
],
},
{
matcher: "/manager/me*",
middlewares: [
authenticate("manager", ["session", "bearer"]),
],
},
],
})
```
Now, manager users can use the `/manager` API route to register, and all routes starting with `/manager/me` are only accessible by authenticated managers.
Find an elaborate example and learn more in [this documentation](../commerce-modules/auth/create-actor-type/page.mdx).
### Apply Promotion on Cart Items and Shipping
To apply a promotion on a cart's items and shipping methods using the [Cart](../commerce-modules/cart/page.mdx) and [Promotion](../commerce-modules/promotion/page.mdx) modules:
```ts
import {
ComputeActionAdjustmentLine,
ComputeActionItemLine,
ComputeActionShippingLine,
AddItemAdjustmentAction,
AddShippingMethodAdjustment,
// ...
} from "@medusajs/framework/types"
// retrieve the cart
const cart = await cartModuleService.retrieveCart("cart_123", {
relations: [
"items.adjustments",
"shipping_methods.adjustments",
],
})
// retrieve line item adjustments
const lineItemAdjustments: ComputeActionItemLine[] = []
cart.items.forEach((item) => {
const filteredAdjustments = item.adjustments?.filter(
(adjustment) => adjustment.code !== undefined
) as unknown as ComputeActionAdjustmentLine[]
if (filteredAdjustments.length) {
lineItemAdjustments.push({
...item,
adjustments: filteredAdjustments,
})
}
})
// retrieve shipping method adjustments
const shippingMethodAdjustments: ComputeActionShippingLine[] =
[]
cart.shipping_methods.forEach((shippingMethod) => {
const filteredAdjustments =
shippingMethod.adjustments?.filter(
(adjustment) => adjustment.code !== undefined
) as unknown as ComputeActionAdjustmentLine[]
if (filteredAdjustments.length) {
shippingMethodAdjustments.push({
...shippingMethod,
adjustments: filteredAdjustments,
})
}
})
// compute actions
const actions = await promotionModuleService.computeActions(
["promo_123"],
{
items: lineItemAdjustments,
shipping_methods: shippingMethodAdjustments,
}
)
// set the adjustments on the line item
await cartModuleService.setLineItemAdjustments(
cart.id,
actions.filter(
(action) => action.action === "addItemAdjustment"
) as AddItemAdjustmentAction[]
)
// set the adjustments on the shipping method
await cartModuleService.setShippingMethodAdjustments(
cart.id,
actions.filter(
(action) =>
action.action === "addShippingMethodAdjustment"
) as AddShippingMethodAdjustment[]
)
```
Learn more in [this documentation](../commerce-modules/cart/tax-lines/page.mdx).
### Retrieve Tax Lines of a Cart's Items and Shipping
To retrieve the tax lines of a cart's items and shipping methods using the [Cart](../commerce-modules/cart/page.mdx) and [Tax](../commerce-modules/tax/page.mdx) modules:
```ts
// retrieve the cart
const cart = await cartModuleService.retrieveCart("cart_123", {
relations: [
"items.tax_lines",
"shipping_methods.tax_lines",
"shipping_address",
],
})
// retrieve the tax lines
const taxLines = await taxModuleService.getTaxLines(
[
...(cart.items as TaxableItemDTO[]),
...(cart.shipping_methods as TaxableShippingDTO[]),
],
{
address: {
...cart.shipping_address,
country_code:
cart.shipping_address.country_code || "us",
},
}
)
// set line item tax lines
await cartModuleService.setLineItemTaxLines(
cart.id,
taxLines.filter((line) => "line_item_id" in line)
)
// set shipping method tax lines
await cartModuleService.setLineItemTaxLines(
cart.id,
taxLines.filter((line) => "shipping_line_id" in line)
)
```
Learn more in [this documentation](../commerce-modules/cart/tax-lines/page.mdx)
### Apply Promotion on an Order's Items and Shipping
To apply a promotion on an order's items and shipping methods using the [Order](../commerce-modules/order/page.mdx) and [Promotion](../commerce-modules/promotion/page.mdx) modules:
```ts
import {
ComputeActionAdjustmentLine,
ComputeActionItemLine,
ComputeActionShippingLine,
AddItemAdjustmentAction,
AddShippingMethodAdjustment,
// ...
} from "@medusajs/framework/types"
// ...
// retrieve the order
const order = await orderModuleService.retrieveOrder("ord_123", {
relations: [
"items.item.adjustments",
"shipping_methods.shipping_method.adjustments",
],
})
// retrieve the line item adjustments
const lineItemAdjustments: ComputeActionItemLine[] = []
order.items.forEach((item) => {
const filteredAdjustments = item.adjustments?.filter(
(adjustment) => adjustment.code !== undefined
) as unknown as ComputeActionAdjustmentLine[]
if (filteredAdjustments.length) {
lineItemAdjustments.push({
...item,
...item.detail,
adjustments: filteredAdjustments,
})
}
})
//retrieve shipping method adjustments
const shippingMethodAdjustments: ComputeActionShippingLine[] =
[]
order.shipping_methods.forEach((shippingMethod) => {
const filteredAdjustments =
shippingMethod.adjustments?.filter(
(adjustment) => adjustment.code !== undefined
) as unknown as ComputeActionAdjustmentLine[]
if (filteredAdjustments.length) {
shippingMethodAdjustments.push({
...shippingMethod,
adjustments: filteredAdjustments,
})
}
})
// compute actions
const actions = await promotionModuleService.computeActions(
["promo_123"],
{
items: lineItemAdjustments,
shipping_methods: shippingMethodAdjustments,
// TODO infer from cart or region
currency_code: "usd",
}
)
// set the adjustments on the line items
await orderModuleService.setOrderLineItemAdjustments(
order.id,
actions.filter(
(action) => action.action === "addItemAdjustment"
) as AddItemAdjustmentAction[]
)
// set the adjustments on the shipping methods
await orderModuleService.setOrderShippingMethodAdjustments(
order.id,
actions.filter(
(action) =>
action.action === "addShippingMethodAdjustment"
) as AddShippingMethodAdjustment[]
)
```
Learn more in [this documentation](../commerce-modules/order/promotion-adjustments/page.mdx)
### Accept Payment using Module
To accept payment using the Payment Module's main service:
1. Create a payment collection and link it to the cart:
```ts
import {
ContainerRegistrationKeys,
Modules,
} from "@medusajs/framework/utils"
// ...
const paymentCollection =
await paymentModuleService.createPaymentCollections({
region_id: "reg_123",
currency_code: "usd",
amount: 5000,
})
// resolve Link
const link = container.resolve(
ContainerRegistrationKeys.LINK
)
// create a link between the cart and payment collection
link.create({
[Modules.CART]: {
cart_id: "cart_123",
},
[Modules.PAYMENT]: {
payment_collection_id: paymentCollection.id,
},
})
```
2. Create a payment session in the collection:
```ts
const paymentSession =
await paymentModuleService.createPaymentSession(
paymentCollection.id,
{
provider_id: "stripe",
currency_code: "usd",
amount: 5000,
data: {
// any necessary data for the
// payment provider
},
}
)
```
3. Authorize the payment session:
```ts
const payment =
await paymentModuleService.authorizePaymentSession(
paymentSession.id,
{}
)
```
Learn more in [this documentation](../commerce-modules/payment/payment-flow/page.mdx).
### Get Variant's Prices for Region and Currency
To get prices of a product variant for a region and currency using [Query](!docs!/learn/fundamentals/module-links/query):
```ts
import { QueryContext } from "@medusajs/framework/utils"
// ...
const { data: products } = await query.graph({
entity: "product",
fields: [
"*",
"variants.*",
"variants.calculated_price.*",
],
filters: {
id: "prod_123",
},
context: {
variants: {
calculated_price: QueryContext({
region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS",
currency_code: "eur",
}),
},
},
})
```
Learn more in [this documentation](../commerce-modules/product/guides/price/page.mdx#retrieve-calculated-price-for-a-context).
### Get All Variant's Prices
To get all prices of a product variant using [Query](!docs!/learn/fundamentals/module-links/query):
```ts
const { data: products } = await query.graph({
entity: "product",
fields: [
"*",
"variants.*",
"variants.prices.*",
],
filters: {
id: [
"prod_123",
],
},
})
```
Learn more in [this documentation](../commerce-modules/product/guides/price/page.mdx).
### Get Variant Prices with Taxes
To get a variant's prices with taxes using [Query](!docs!/learn/fundamentals/module-links/query) and the [Tax Module](../commerce-modules/tax/page.mdx)
```ts
import {
HttpTypes,
TaxableItemDTO,
ItemTaxLineDTO,
} from "@medusajs/framework/types"
import {
QueryContext,
calculateAmountsWithTax,
} from "@medusajs/framework/utils"
// other imports...
// ...
const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => {
return product.variants
?.map((variant) => {
if (!variant.calculated_price) {
return
}
return {
id: variant.id,
product_id: product.id,
product_name: product.title,
product_categories: product.categories?.map((c) => c.name),
product_category_id: product.categories?.[0]?.id,
product_sku: variant.sku,
product_type: product.type,
product_type_id: product.type_id,
quantity: 1,
unit_price: variant.calculated_price.calculated_amount,
currency_code: variant.calculated_price.currency_code,
}
})
.filter((v) => !!v) as unknown as TaxableItemDTO[]
}
const { data: products } = await query.graph({
entity: "product",
fields: [
"*",
"variants.*",
"variants.calculated_price.*",
],
filters: {
id: "prod_123",
},
context: {
variants: {
calculated_price: QueryContext({
region_id: "region_123",
currency_code: "usd",
}),
},
},
})
const taxLines = (await taxModuleService.getTaxLines(
products.map(asTaxItem).flat(),
{
// example of context properties. You can pass other ones.
address: {
country_code,
},
}
)) as unknown as ItemTaxLineDTO[]
const taxLinesMap = new Map<string, ItemTaxLineDTO[]>()
taxLines.forEach((taxLine) => {
const variantId = taxLine.line_item_id
if (!taxLinesMap.has(variantId)) {
taxLinesMap.set(variantId, [])
}
taxLinesMap.get(variantId)?.push(taxLine)
})
products.forEach((product) => {
product.variants?.forEach((variant) => {
if (!variant.calculated_price) {
return
}
const taxLinesForVariant = taxLinesMap.get(variant.id) || []
const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({
taxLines: taxLinesForVariant,
amount: variant.calculated_price!.calculated_amount!,
includesTax:
variant.calculated_price!.is_calculated_price_tax_inclusive!,
})
// do something with prices...
})
})
```
Learn more in [this documentation](../commerce-modules/product/guides/price-with-taxes/page.mdx).
### Invite Users
To invite a user using the [User Module](../commerce-modules/user/page.mdx):
```ts
const invite = await userModuleService.createInvites({
email: "user@example.com",
})
```
### Accept User Invite
To accept an invite and create a user using the [User Module](../commerce-modules/user/page.mdx):
```ts
const invite =
await userModuleService.validateInviteToken(inviteToken)
await userModuleService.updateInvites({
id: invite.id,
accepted: true,
})
const user = await userModuleService.createUsers({
email: invite.email,
})
```