diff --git a/.changeset/good-wolves-travel.md b/.changeset/good-wolves-travel.md new file mode 100644 index 0000000000000..b3375938da166 --- /dev/null +++ b/.changeset/good-wolves-travel.md @@ -0,0 +1,8 @@ +--- +"@medusajs/workflow-engine-inmemory": patch +"@medusajs/workflow-engine-redis": patch +"@medusajs/orchestration": patch +"@medusajs/types": patch +--- + +chore(workflow-engine): expose cancel method diff --git a/packages/admin/dashboard/src/routes/product-variants/product-variant-manage-inventory-items/components/manage-variant-inventory-items-form/manage-variant-inventory-items-form.tsx b/packages/admin/dashboard/src/routes/product-variants/product-variant-manage-inventory-items/components/manage-variant-inventory-items-form/manage-variant-inventory-items-form.tsx index a5799a1deeca7..6f1a4a8fe0c01 100644 --- a/packages/admin/dashboard/src/routes/product-variants/product-variant-manage-inventory-items/components/manage-variant-inventory-items-form/manage-variant-inventory-items-form.tsx +++ b/packages/admin/dashboard/src/routes/product-variants/product-variant-manage-inventory-items/components/manage-variant-inventory-items-form/manage-variant-inventory-items-form.tsx @@ -91,7 +91,7 @@ export function ManageVariantInventoryItemsForm({ queryFn: (params) => sdk.admin.inventoryItem.list(params), getOptions: (data) => data.inventory_items.map((item) => ({ - label: item.title || item.sku!, + label: `${item.title} (${item.sku})`, value: item.id!, })), defaultValue: variant.inventory_items?.[0]?.inventory_item_id, diff --git a/packages/admin/dashboard/src/routes/products/product-create/components/product-create-inventory-kit-form/components/product-create-inventory-kit-section/product-create-inventory-kit-section.tsx b/packages/admin/dashboard/src/routes/products/product-create/components/product-create-inventory-kit-form/components/product-create-inventory-kit-section/product-create-inventory-kit-section.tsx index 2728ec3e5b3eb..eb045a4f9ec7d 100644 --- a/packages/admin/dashboard/src/routes/products/product-create/components/product-create-inventory-kit-form/components/product-create-inventory-kit-section/product-create-inventory-kit-section.tsx +++ b/packages/admin/dashboard/src/routes/products/product-create/components/product-create-inventory-kit-form/components/product-create-inventory-kit-section/product-create-inventory-kit-section.tsx @@ -33,7 +33,7 @@ function VariantSection({ form, variant, index }: VariantSectionProps) { queryFn: (params) => sdk.admin.inventoryItem.list(params), getOptions: (data) => data.inventory_items.map((item) => ({ - label: item.title, + label: `${item.title} (${item.sku})`, value: item.id, })), }) diff --git a/packages/core/orchestration/src/transaction/transaction-orchestrator.ts b/packages/core/orchestration/src/transaction/transaction-orchestrator.ts index a2d70e9889e4d..f729bb05825b2 100644 --- a/packages/core/orchestration/src/transaction/transaction-orchestrator.ts +++ b/packages/core/orchestration/src/transaction/transaction-orchestrator.ts @@ -20,6 +20,7 @@ import { import { isDefined, isErrorLike, + isObject, MedusaError, promiseAll, serializeError, @@ -188,6 +189,7 @@ export class TransactionOrchestrator extends EventEmitter { TransactionStepState.DORMANT, TransactionStepState.SKIPPED, ] + const siblings = step.next.map((sib) => flow.steps[sib]) return ( siblings.length === 0 || @@ -1208,70 +1210,72 @@ export class TransactionOrchestrator extends EventEmitter { while (queue.length > 0) { const { obj, level } = queue.shift() - for (const key of Object.keys(obj)) { - if (typeof obj[key] === "object" && obj[key] !== null) { - queue.push({ obj: obj[key], level: [...level] }) - } else if (key === "action") { - if (actionNames.has(obj.action)) { - throw new Error( - `Step ${obj.action} is already defined in workflow.` - ) - } + if (obj.action) { + if (actionNames.has(obj.action)) { + throw new Error(`Step ${obj.action} is already defined in workflow.`) + } - actionNames.add(obj.action) - level.push(obj.action) - const id = level.join(".") - const parent = level.slice(0, level.length - 1).join(".") + actionNames.add(obj.action) + level.push(obj.action) + const id = level.join(".") + const parent = level.slice(0, level.length - 1).join(".") - if (!existingSteps || parent === TransactionOrchestrator.ROOT_STEP) { - states[parent].next?.push(id) - } + if (!existingSteps || parent === TransactionOrchestrator.ROOT_STEP) { + states[parent].next?.push(id) + } - const definitionCopy = { ...obj } - delete definitionCopy.next + const definitionCopy = { ...obj } + delete definitionCopy.next - if (definitionCopy.async) { - features.hasAsyncSteps = true - } + if (definitionCopy.async) { + features.hasAsyncSteps = true + } - if (definitionCopy.timeout) { - features.hasStepTimeouts = true - } + if (definitionCopy.timeout) { + features.hasStepTimeouts = true + } - if ( - definitionCopy.retryInterval || - definitionCopy.retryIntervalAwaiting - ) { - features.hasRetriesTimeout = true - } + if ( + definitionCopy.retryInterval || + definitionCopy.retryIntervalAwaiting + ) { + features.hasRetriesTimeout = true + } + + if (definitionCopy.nested) { + features.hasNestedTransactions = true + } - if (definitionCopy.nested) { - features.hasNestedTransactions = true + states[id] = Object.assign( + new TransactionStep(), + existingSteps?.[id] || { + id, + uuid: definitionCopy.uuid, + depth: level.length - 1, + definition: definitionCopy, + saveResponse: definitionCopy.saveResponse ?? true, + invoke: { + state: TransactionStepState.NOT_STARTED, + status: TransactionStepStatus.IDLE, + }, + compensate: { + state: TransactionStepState.DORMANT, + status: TransactionStepStatus.IDLE, + }, + attempts: 0, + failures: 0, + lastAttempt: null, + next: [], } + ) + } - states[id] = Object.assign( - new TransactionStep(), - existingSteps?.[id] || { - id, - uuid: definitionCopy.uuid, - depth: level.length - 1, - definition: definitionCopy, - saveResponse: definitionCopy.saveResponse ?? true, - invoke: { - state: TransactionStepState.NOT_STARTED, - status: TransactionStepStatus.IDLE, - }, - compensate: { - state: TransactionStepState.DORMANT, - status: TransactionStepStatus.IDLE, - }, - attempts: 0, - failures: 0, - lastAttempt: null, - next: [], - } - ) + if (Array.isArray(obj.next)) { + for (const next of obj.next) { + queue.push({ obj: next, level: [...level] }) } + } else if (isObject(obj.next)) { + queue.push({ obj: obj.next, level: [...level] }) } } diff --git a/packages/core/types/src/workflows-sdk/service.ts b/packages/core/types/src/workflows-sdk/service.ts index 59e719c3bac4e..a2a95ab6de361 100644 --- a/packages/core/types/src/workflows-sdk/service.ts +++ b/packages/core/types/src/workflows-sdk/service.ts @@ -1,5 +1,5 @@ import { FindConfig } from "../common" -import { IModuleService } from "../modules-sdk" +import { ContainerLike, IModuleService } from "../modules-sdk" import { Context } from "../shared-context" import { FilterableWorkflowExecutionProps, @@ -28,6 +28,15 @@ export interface WorkflowOrchestratorRunDTO transactionId?: string } +export interface WorkflowOrchestratorCancelOptionsDTO { + transactionId: string + context?: Context + throwOnError?: boolean + logOnError?: boolean + events?: Record + container?: ContainerLike +} + export type IdempotencyKeyParts = { workflowId: string transactionId: string @@ -63,17 +72,11 @@ export interface IWorkflowEngineService extends IModuleService { workflowId: string, options?: WorkflowOrchestratorRunDTO, sharedContext?: Context - ): Promise<{ - errors: Error[] - transaction: object - result: any - acknowledgement: Acknowledgement - }> + ) getRunningTransaction( workflowId: string, transactionId: string, - options?: Record, sharedContext?: Context ): Promise @@ -121,4 +124,10 @@ export interface IWorkflowEngineService extends IModuleService { }, sharedContext?: Context ) + + cancel( + workflowId: string, + options: WorkflowOrchestratorCancelOptionsDTO, + sharedContext?: Context + ) } diff --git a/packages/modules/workflow-engine-inmemory/integration-tests/__fixtures__/index.ts b/packages/modules/workflow-engine-inmemory/integration-tests/__fixtures__/index.ts index fee018fbdca91..87a9596f4a449 100644 --- a/packages/modules/workflow-engine-inmemory/integration-tests/__fixtures__/index.ts +++ b/packages/modules/workflow-engine-inmemory/integration-tests/__fixtures__/index.ts @@ -4,4 +4,5 @@ export * from "./workflow_async" export * from "./workflow_conditional_step" export * from "./workflow_idempotent" export * from "./workflow_step_timeout" +export * from "./workflow_sync" export * from "./workflow_transaction_timeout" diff --git a/packages/modules/workflow-engine-inmemory/integration-tests/__fixtures__/workflow_sync.ts b/packages/modules/workflow-engine-inmemory/integration-tests/__fixtures__/workflow_sync.ts new file mode 100644 index 0000000000000..40fc5fb822c2e --- /dev/null +++ b/packages/modules/workflow-engine-inmemory/integration-tests/__fixtures__/workflow_sync.ts @@ -0,0 +1,64 @@ +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step_1 = createStep( + "step_1", + jest.fn((input) => { + input.test = "test" + return new StepResponse(input, { compensate: 123 }) + }), + jest.fn((compensateInput) => { + if (!compensateInput) { + return + } + + return new StepResponse({ + reverted: true, + }) + }) +) + +const step_2 = createStep( + "step_2", + jest.fn((input, context) => { + if (input) { + return new StepResponse({ notAsyncResponse: input.hey }) + } + }), + jest.fn((_, context) => { + return new StepResponse({ + step: context.metadata.action, + idempotency_key: context.metadata.idempotency_key, + reverted: true, + }) + }) +) + +const step_3 = createStep( + "step_3", + jest.fn((res) => { + return new StepResponse({ + done: { + inputFromSyncStep: res.notAsyncResponse, + }, + }) + }) +) + +createWorkflow( + { + name: "workflow_sync", + idempotent: true, + }, + function (input) { + step_1(input) + + const ret2 = step_2({ hey: "oh" }) + + return new WorkflowResponse(step_3(ret2)) + } +) diff --git a/packages/modules/workflow-engine-inmemory/integration-tests/__tests__/index.spec.ts b/packages/modules/workflow-engine-inmemory/integration-tests/__tests__/index.spec.ts index 241e4da9ed971..cc2d482f2b9cb 100644 --- a/packages/modules/workflow-engine-inmemory/integration-tests/__tests__/index.spec.ts +++ b/packages/modules/workflow-engine-inmemory/integration-tests/__tests__/index.spec.ts @@ -300,6 +300,26 @@ moduleIntegrationTestRunner({ expect(onFinish).toHaveBeenCalledTimes(0) }) + it("should cancel and revert a completed workflow", async () => { + const workflowId = "workflow_sync" + + const { acknowledgement, transaction: trx } = + await workflowOrcModule.run(workflowId, { + input: { + value: "123", + }, + }) + + expect(trx.getFlow().state).toEqual("done") + expect(acknowledgement.hasFinished).toBe(true) + + const { transaction } = await workflowOrcModule.cancel(workflowId, { + transactionId: acknowledgement.transactionId, + }) + + expect(transaction.getFlow().state).toEqual("reverted") + }) + it("should run conditional steps if condition is true", (done) => { void workflowOrcModule.subscribe({ workflowId: "workflow_conditional_step", diff --git a/packages/modules/workflow-engine-inmemory/src/services/workflow-orchestrator.ts b/packages/modules/workflow-engine-inmemory/src/services/workflow-orchestrator.ts index 02586d8a6eeea..483de93037838 100644 --- a/packages/modules/workflow-engine-inmemory/src/services/workflow-orchestrator.ts +++ b/packages/modules/workflow-engine-inmemory/src/services/workflow-orchestrator.ts @@ -6,7 +6,11 @@ import { TransactionStep, WorkflowScheduler, } from "@medusajs/framework/orchestration" -import { ContainerLike, MedusaContainer } from "@medusajs/framework/types" +import { + ContainerLike, + Context, + MedusaContainer, +} from "@medusajs/framework/types" import { isString, MedusaError, @@ -18,9 +22,9 @@ import { resolveValue, ReturnWorkflow, } from "@medusajs/framework/workflows-sdk" +import { WorkflowOrchestratorCancelOptions } from "@types" import { ulid } from "ulid" import { InMemoryDistributedTransactionStorage } from "../utils" -import { WorkflowOrchestratorCancelOptions } from "@types" export type WorkflowOrchestratorRunOptions = Omit< FlowRunOptions, @@ -319,10 +323,8 @@ export class WorkflowOrchestratorService { async getRunningTransaction( workflowId: string, transactionId: string, - options?: WorkflowOrchestratorRunOptions + context?: Context ): Promise { - let { context, container } = options ?? {} - if (!workflowId) { throw new Error("Workflow ID is required") } @@ -339,9 +341,7 @@ export class WorkflowOrchestratorService { throw new Error(`Workflow with id "${workflowId}" not found.`) } - const flow = exportedWorkflow( - (container as MedusaContainer) ?? this.container_ - ) + const flow = exportedWorkflow() const transaction = await flow.getRunningTransaction(transactionId, context) diff --git a/packages/modules/workflow-engine-inmemory/src/services/workflows-module.ts b/packages/modules/workflow-engine-inmemory/src/services/workflows-module.ts index 07d1c3aa5997b..b397b83d8399e 100644 --- a/packages/modules/workflow-engine-inmemory/src/services/workflows-module.ts +++ b/packages/modules/workflow-engine-inmemory/src/services/workflows-module.ts @@ -18,6 +18,7 @@ import type { import { SqlEntityManager } from "@mikro-orm/postgresql" import { WorkflowExecution } from "@models" import { WorkflowOrchestratorService } from "@services" +import { WorkflowOrchestratorCancelOptions } from "@types" type InjectedDependencies = { manager: SqlEntityManager @@ -185,4 +186,16 @@ export class WorkflowsModuleService< updated_at <= (CURRENT_TIMESTAMP - INTERVAL '1 second' * retention_time); `) } + + @InjectSharedContext() + async cancel>( + workflowIdOrWorkflow: TWorkflow, + options: WorkflowOrchestratorCancelOptions, + @MedusaContext() context: Context = {} + ) { + return this.workflowOrchestratorService_.cancel( + workflowIdOrWorkflow, + options + ) + } } diff --git a/packages/modules/workflow-engine-inmemory/src/types/index.ts b/packages/modules/workflow-engine-inmemory/src/types/index.ts index 7a7ac40112a40..272f33d10ae5b 100644 --- a/packages/modules/workflow-engine-inmemory/src/types/index.ts +++ b/packages/modules/workflow-engine-inmemory/src/types/index.ts @@ -8,7 +8,8 @@ export type InitializeModuleInjectableDependencies = { export type WorkflowOrchestratorCancelOptions = Omit< FlowCancelOptions, - "transaction" | "container" + "transaction" | "transactionId" | "container" > & { + transactionId: string container?: ContainerLike } diff --git a/packages/modules/workflow-engine-redis/integration-tests/__fixtures__/index.ts b/packages/modules/workflow-engine-redis/integration-tests/__fixtures__/index.ts index f3183ed07013a..3b47cf4e8d216 100644 --- a/packages/modules/workflow-engine-redis/integration-tests/__fixtures__/index.ts +++ b/packages/modules/workflow-engine-redis/integration-tests/__fixtures__/index.ts @@ -1,7 +1,8 @@ export * from "./workflow_1" export * from "./workflow_2" export * from "./workflow_async" +export * from "./workflow_async_compensate" export * from "./workflow_step_timeout" +export * from "./workflow_sync" export * from "./workflow_transaction_timeout" export * from "./workflow_when" -export * from "./workflow_async_compensate" diff --git a/packages/modules/workflow-engine-redis/integration-tests/__fixtures__/workflow_sync.ts b/packages/modules/workflow-engine-redis/integration-tests/__fixtures__/workflow_sync.ts new file mode 100644 index 0000000000000..40fc5fb822c2e --- /dev/null +++ b/packages/modules/workflow-engine-redis/integration-tests/__fixtures__/workflow_sync.ts @@ -0,0 +1,64 @@ +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step_1 = createStep( + "step_1", + jest.fn((input) => { + input.test = "test" + return new StepResponse(input, { compensate: 123 }) + }), + jest.fn((compensateInput) => { + if (!compensateInput) { + return + } + + return new StepResponse({ + reverted: true, + }) + }) +) + +const step_2 = createStep( + "step_2", + jest.fn((input, context) => { + if (input) { + return new StepResponse({ notAsyncResponse: input.hey }) + } + }), + jest.fn((_, context) => { + return new StepResponse({ + step: context.metadata.action, + idempotency_key: context.metadata.idempotency_key, + reverted: true, + }) + }) +) + +const step_3 = createStep( + "step_3", + jest.fn((res) => { + return new StepResponse({ + done: { + inputFromSyncStep: res.notAsyncResponse, + }, + }) + }) +) + +createWorkflow( + { + name: "workflow_sync", + idempotent: true, + }, + function (input) { + step_1(input) + + const ret2 = step_2({ hey: "oh" }) + + return new WorkflowResponse(step_3(ret2)) + } +) diff --git a/packages/modules/workflow-engine-redis/integration-tests/__tests__/index.spec.ts b/packages/modules/workflow-engine-redis/integration-tests/__tests__/index.spec.ts index a3410fb009b8a..16f01254ace0d 100644 --- a/packages/modules/workflow-engine-redis/integration-tests/__tests__/index.spec.ts +++ b/packages/modules/workflow-engine-redis/integration-tests/__tests__/index.spec.ts @@ -512,6 +512,26 @@ moduleIntegrationTestRunner({ failTrap(done) }) + + it("should cancel and revert a completed workflow", async () => { + const workflowId = "workflow_sync" + + const { acknowledgement, transaction: trx } = + await workflowOrcModule.run(workflowId, { + input: { + value: "123", + }, + }) + + expect(trx.getFlow().state).toEqual("done") + expect(acknowledgement.hasFinished).toBe(true) + + const { transaction } = await workflowOrcModule.cancel(workflowId, { + transactionId: acknowledgement.transactionId, + }) + + expect(transaction.getFlow().state).toEqual("reverted") + }) }) // Note: These tests depend on actual Redis instance and waiting for the scheduled jobs to run, which isn't great. diff --git a/packages/modules/workflow-engine-redis/src/services/workflow-orchestrator.ts b/packages/modules/workflow-engine-redis/src/services/workflow-orchestrator.ts index b5293cc4828af..eef069c029fc8 100644 --- a/packages/modules/workflow-engine-redis/src/services/workflow-orchestrator.ts +++ b/packages/modules/workflow-engine-redis/src/services/workflow-orchestrator.ts @@ -35,8 +35,11 @@ export type WorkflowOrchestratorRunOptions = Omit< export type WorkflowOrchestratorCancelOptions = Omit< FlowCancelOptions, - "transaction" -> + "transaction" | "transactionId" | "container" +> & { + transactionId: string + container?: ContainerLike +} type RegisterStepSuccessOptions = Omit< WorkflowOrchestratorRunOptions, @@ -379,10 +382,8 @@ export class WorkflowOrchestratorService { async getRunningTransaction( workflowId: string, transactionId: string, - options?: { context?: Context } + context?: Context ): Promise { - let { context } = options ?? {} - if (!workflowId) { throw new Error("Workflow ID is required") } @@ -398,10 +399,9 @@ export class WorkflowOrchestratorService { throw new Error(`Workflow with id "${workflowId}" not found.`) } - const transaction = await exportedWorkflow.getRunningTransaction( - transactionId, - context - ) + const flow = exportedWorkflow() + + const transaction = await flow.getRunningTransaction(transactionId, context) return transaction } diff --git a/packages/modules/workflow-engine-redis/src/services/workflows-module.ts b/packages/modules/workflow-engine-redis/src/services/workflows-module.ts index 2795c4f7e506a..ff1a71a605269 100644 --- a/packages/modules/workflow-engine-redis/src/services/workflows-module.ts +++ b/packages/modules/workflow-engine-redis/src/services/workflows-module.ts @@ -17,7 +17,10 @@ import type { } from "@medusajs/framework/workflows-sdk" import { SqlEntityManager } from "@mikro-orm/postgresql" import { WorkflowExecution } from "@models" -import { WorkflowOrchestratorService } from "@services" +import { + WorkflowOrchestratorCancelOptions, + WorkflowOrchestratorService, +} from "@services" type InjectedDependencies = { manager: SqlEntityManager @@ -112,7 +115,7 @@ export class WorkflowsModuleService< return await this.workflowOrchestratorService_.getRunningTransaction( workflowId, transactionId, - { context } + context ) } @@ -194,4 +197,13 @@ export class WorkflowsModuleService< updated_at <= (CURRENT_TIMESTAMP - INTERVAL '1 second' * retention_time); `) } + + @InjectSharedContext() + async cancel( + workflowId: string, + options: WorkflowOrchestratorCancelOptions, + @MedusaContext() context: Context = {} + ) { + return this.workflowOrchestratorService_.cancel(workflowId, options) + } } diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index bdacfe55c2718..83dc5c6fc444d 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -458,48 +458,133 @@ npm install ``` -# Using TypeScript Aliases +# Build Custom Features -By default, Medusa doesn't support TypeScript aliases in production. +In the upcoming chapters, you'll follow step-by-step guides to build custom features in Medusa. These guides gradually introduce Medusa's concepts to help you understand what they are and how to use them. -If you prefer using TypeScript aliases, install following development dependencies: +By following these guides, you'll add brands to the Medusa application that you can associate with products. -```bash npm2yarn -npm install --save-dev tsc-alias rimraf -``` +To build a custom feature in Medusa, you need three main tools: -Where `tsc-alias` is a package that resolves TypeScript aliases, and `rimraf` is a package that removes files and directories. +- [Module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md): a package with commerce logic for a single domain. It defines new tables to add to the database, and a class of methods to manage these tables. +- [Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md): a tool to perform an operation comprising multiple steps with built-in rollback and retry mechanisms. +- [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md): a REST endpoint that exposes commerce features to clients, such as the admin dashboard or a storefront. The API route executes a workflow that implements the commerce feature using modules. -Then, add a new `resolve:aliases` script to your `package.json` and update the `build` script: +![Diagram showcasing the flow of a custom developed feature](https://res.cloudinary.com/dza7lstvk/image/upload/v1725867628/Medusa%20Book/custom-development_nofvp6.jpg) -```json title="package.json" -{ - "scripts": { - // other scripts... - "resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json", - "build": "npm run resolve:aliases && medusa build" - } -} -``` +*** -You can now use TypeScript aliases in your Medusa application. For example, add the following in `tsconfig.json`: +## Next Chapters: Brand Module Example -```json title="tsconfig.json" -{ - "compilerOptions": { - // ... - "paths": { - "@/*": ["./src/*"] - } - } -} -``` +The next chapters will guide you to: -Now, you can import modules, for example, using TypeScript aliases: +1. Build 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. -```ts -import { BrandModuleService } from "@/modules/brand/service" -``` + +# Customize Medusa Admin Dashboard + +In the previous chapters, you've customized your Medusa application to [add brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), [expose an API route to create brands](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), and [linked brands to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md). + +After customizing and extending your application with new features, you may need to provide an interface for admin users to utilize these features. The Medusa Admin dashboard is extendable, allowing you to: + +- Insert components, called [widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md), on existing pages. +- Add new pages, called [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). + +From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard + +*** + +## Next Chapters: View Brands in Dashboard + +In the next chapters, you'll continue with the brands example to: + +- Add a new section to the product details page that shows the product's brand. +- Add a new page in the dashboard that shows all brands in the store. + + +# Extend Core Commerce Features + +In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features. + +In other commerce platforms, you extend core features and models through hacky workarounds that can introduce unexpected issues and side effects across the platform. It also makes your application difficult to maintain and upgrade in the long run. + +Medusa's framework and orchestration tools mitigate these issues while supporting all your customization needs: + +- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md): Link data models of different modules without building direct dependencies, ensuring that the Medusa application integrates your modules without side effects. +- [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md): inject custom functionalities into a workflow at predefined points, called hooks. This allows you to perform custom actions as a part of a core workflow without hacky workarounds. +- [Additional Data in API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md): Configure core API routes to accept request parameters relevant to your customizations. These parameters are passed to the underlying workflow's hooks, where you can manage your custom data as part of an existing flow. + +*** + +## Next Chapters: Link Brands to Products Example + +The next chapters explain how to use the tools mentioned above with step-by-step guides. You'll continue with the [brands example from the previous chapters](https://docs.medusajs.com/learn/customization/custom-features/index.html.md) to: + +- Link brands from the custom [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to products from Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md). +- Extend the core product-creation workflow and the API route that uses it to allow setting the brand of a newly created product. +- Retrieve a product's associated brand's details. + + +# Customizations Next Steps: Learn the Fundamentals + +The previous guides introduced Medusa's different concepts and how you can use them to customize Medusa for a realistic use case, You added brands to your application, linked them to products, customized the admin dashboard, and integrated a third-party CMS. + +The next chapters will cover each of these concepts in depth, with the different ways you can use them, their options or configurations, and more advanced features that weren't covered in the previous guides. While you can start building with Medusa, it's highly recommended to follow the next chapters for a better understanding of Medusa's fundamentals. + +## Useful Guides + +The following guides and references are useful for your development journey: + +3. [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md): Browse the list of commerce modules in Medusa and their references to learn how to use them. +4. [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md): Learn about the methods generated by `MedusaService` with examples. +5. [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md): Browse the list of core workflows and their hooks that are useful for your customizations. +6. [Admin Injection Zones](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md): Browse the injection zones in the Medusa Admin to learn where you can inject widgets. + +*** + +## More Examples in Recipes + +In the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more. + + +# Re-Use Customizations with Plugins + +In the previous chapters, you've learned important concepts related to creating modules, implementing commerce features in workflows, exposing those features in API routes, customizing the Medusa Admin dashboard with Admin Extensions, and integrating third-party systems. + +You've implemented the brands example within a single Medusa application. However, this approach is not scalable when you want to reuse your customizations across multiple projects. + +To reuse your customizations across multiple Medusa applications, such as implementing brands in different projects, you can create a plugin. A plugin is an NPM package that encapsulates your customizations and can be installed in any Medusa application. Plugins can include modules, workflows, API routes, Admin Extensions, and more. + +![Diagram showcasing how the Brand Plugin would add its resources to any application it's installed in](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540091/Medusa%20Book/brand-plugin_bk9zi9.jpg) + +Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM. + +To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + + +# Integrate Third-Party Systems + +Commerce applications often connect to third-party systems that provide additional or specialized features. For example, you may integrate a Content-Management System (CMS) for rich content features, a payment provider to process credit-card payments, and a notification service to send emails. + +Medusa's framework facilitates integrating these systems and orchestrating operations across them, saving you the effort of managing them yourself. You won't find those capabilities in other commerce platforms that in these scenarios become a bottleneck to building customizations and iterating quickly. + +In Medusa, you integrate a third-party system by: + +1. Creating a module whose service provides the methods to connect to and perform operations in the third-party system. +2. Building workflows that complete tasks spanning across systems. You use the module that integrates a third-party system in the workflow's steps. +3. Executing the workflows you built in an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), at a scheduled time, or when an event is emitted. + +*** + +## Next Chapters: Sync Brands Example + +In the previous chapters, you've [added brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to your Medusa application. In the next chapters, you will: + +1. Integrate a dummy third-party CMS in the Brand Module. +2. Sync brands to the CMS when a brand is created. +3. Sync brands from the CMS at a daily schedule. # Medusa Application Configuration @@ -1405,30 +1490,6 @@ npx medusa db:migrate ``` -# Admin Development - -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/index.html.md) 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](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. - - # General Medusa Application Deployment Guide In this document, you'll learn the general steps to deploy your Medusa application. How you apply these steps depend on your chosen hosting provider or platform. @@ -1728,63 +1789,72 @@ Replace the email `admin-medusa@test.com` and password `supersecret` with the cr You can use these credentials to log into the Medusa Admin dashboard. -# API Routes +# Using TypeScript Aliases -In this chapter, you’ll learn what API Routes are and how to create them. +By default, Medusa doesn't support TypeScript aliases in production. -## What is an API Route? +If you prefer using TypeScript aliases, install following development dependencies: -An API Route is an endpoint. It exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems. +```bash npm2yarn +npm install --save-dev tsc-alias rimraf +``` -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. +Where `tsc-alias` is a package that resolves TypeScript aliases, and `rimraf` is a package that removes files and directories. -*** +Then, add a new `resolve:aliases` script to your `package.json` and update the `build` script: -## How to Create an API Route? +```json title="package.json" +{ + "scripts": { + // other scripts... + "resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json", + "build": "npm run resolve:aliases && medusa build" + } +} +``` -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`. +You can now use TypeScript aliases in your Medusa application. For example, add the following in `tsconfig.json`: -![Example of API route in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732808645/Medusa%20Book/route-dir-overview_dqgzmk.jpg) +```json title="tsconfig.json" +{ + "compilerOptions": { + // ... + "paths": { + "@/*": ["./src/*"] + } + } +} +``` -Each file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). +Now, you can import modules, for example, using TypeScript aliases: -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 +import { BrandModuleService } from "@/modules/brand/service" +``` -```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!", - }) -} -``` +# Admin Development -### Test API Route +In the next chapters, you'll learn more about possible admin customizations. -To test the API route above, start the Medusa application: +You can customize the admin dashboard by: -```bash npm2yarn -npm run dev -``` +- Adding new sections to existing pages using Widgets. +- Adding new pages using UI Routes. -Then, send a `GET` request to the `/hello-world` API Route: +*** -```bash -curl http://localhost:9000/hello-world -``` +## 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/index.html.md) to learn how to install it and use its components. *** -## When to Use API Routes +## Admin Components List -You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application. +To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. # Custom CLI Scripts @@ -1857,106 +1927,63 @@ npx medusa exec ./src/scripts/my-script.ts arg1 arg2 ``` -# Events and Subscribers +# API Routes -In this chapter, you’ll learn about Medusa's event system, and how to handle events with subscribers. - -## Handle Core Commerce Flows with Events - -When building commerce digital applications, you'll often need to perform an action after a commerce operation is performed. For example, sending an order confirmation email when the customer places an order, or syncing data that's updated in Medusa to a third-party system. - -Medusa emits events when core commerce features are performed, and you can listen to and handle these events in asynchronous functions. You can think of Medusa's events like you'd think about webhooks in other commerce platforms, but instead of having to setup separate applications to handle webhooks, your efforts only go into writing the logic right in your Medusa codebase. - -You listen to an event in a subscriber, which is an asynchronous function that's executed when its associated event is emitted. - -![A diagram showcasing an example of how an event is emitted when an order is placed.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732277948/Medusa%20Book/order-placed-event-example_e4e4kw.jpg) - -Subscribers are useful to perform actions that aren't integral to the original flow. For example, you can handle the `order.placed` event in a subscriber that sends a confirmation email to the customer. The subscriber has no impact on the original order-placement flow, as it's executed outside of it. +In this chapter, you’ll learn what API Routes are and how to create them. -If the action you're performing is integral to the main flow of the core commerce feature, use [workflow hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) instead. +## What is an API Route? -### List of Emitted Events +An API Route is an endpoint. It exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems. -Find a list of all emitted events in [this reference](https://docs.medusajs.com/resources/events-reference/index.html.md). +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 a Subscriber? - -You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. The file exports the function to execute and the subscriber's configuration that indicate what event(s) it listens to. - -For example, create the file `src/subscribers/order-placed.ts` with the following content: +## How to Create an API Route? -![Example of subscriber file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866244/Medusa%20Book/subscriber-dir-overview_pusyeu.jpg) +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`. -```ts title="src/subscribers/product-created.ts" -import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" -import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation" +![Example of API route in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732808645/Medusa%20Book/route-dir-overview_dqgzmk.jpg) -export default async function orderPlacedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const logger = container.resolve("logger") +Each file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). - logger.info("Sending confirmation email...") +For example, to create a `GET` API Route at `/hello-world`, create the file `src/api/hello-world/route.ts` with the following content: - await sendOrderConfirmationWorkflow(container) - .run({ - input: { - id: data.id, - }, - }) -} +```ts title="src/api/hello-world/route.ts" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -export const config: SubscriberConfig = { - event: `order.placed`, +export const GET = ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "[GET] Hello world!", + }) } ``` -This subscriber file exports: - -- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered. -- A configuration object with an `event` property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber. - -The subscriber function receives an object as a parameter that has the following properties: - -- `event`: An object with the event's details. The `data` property contains the data payload of the event emitted, which is the order's ID in this case. -- `container`: The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) that you can use to resolve registered resources. - -In the subscriber function, you use the container to resolve the Logger utility and log a message in the console. Also, assuming you have a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that sends an order confirmation email, you execute it in the subscriber. - -*** - -## Test the Subscriber +### Test API Route -To test the subscriber, start the Medusa application: +To test the API route above, start the Medusa application: ```bash npm2yarn npm run dev ``` -Then, try placing an order either using Medusa's API routes or the [Next.js Storefront](https://docs.medusajs.com/learn/storefront-development/nextjs-starter/index.html.md). You'll see the following message in the terminal: +Then, send a `GET` request to the `/hello-world` API Route: ```bash -info: Processing order.placed which has 1 subscribers -Sending confirmation email... +curl http://localhost:9000/hello-world ``` -The first message indicates that the `order.placed` event was emitted, and the second one is the message logged from the subscriber. - *** -## Event Module - -The subscription and emitting of events is handled by an Event Module, an architectural module that implements the pub/sub functionalities of Medusa's event system. - -Medusa provides two Event Modules out of the box: - -- [Local Event Module](https://docs.medusajs.com/resources/architectural-modules/event/local/index.html.md), used by default. It's useful for development, as you don't need additional setup to use it. -- [Redis Event Module](https://docs.medusajs.com/resources/architectural-modules/event/redis/index.html.md), which is useful in production. It uses [Redis](https://redis.io/) to implement Medusa's pub/sub events system. +## When to Use API Routes -Medusa's [architecture](https://docs.medusajs.com/learn/introduction/architecture/index.html.md) also allows you to build a custom Event Module that uses a different service or logic to implement the pub/sub system. Learn how to build an Event Module in [this guide](https://docs.medusajs.com/resources/architectural-modules/event/create/index.html.md). +You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application. # Environment Variables @@ -2038,19 +2065,6 @@ You should opt for setting configurations in `medusa-config.ts` where possible. ||Whether to disable analytics data collection. Learn more in || -# Data Models Advanced Guides - -Data models are created and managed in a module. To learn how to create a data model in a custom module, refer to the [Modules chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -In the next chapters, you'll learn about defining data models in more details. 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. - - # Medusa Container In this chapter, you’ll learn about the Medusa container and how to use it. @@ -2201,6 +2215,121 @@ A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), Learn more about the module's container in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md). +# Events and Subscribers + +In this chapter, you’ll learn about Medusa's event system, and how to handle events with subscribers. + +## Handle Core Commerce Flows with Events + +When building commerce digital applications, you'll often need to perform an action after a commerce operation is performed. For example, sending an order confirmation email when the customer places an order, or syncing data that's updated in Medusa to a third-party system. + +Medusa emits events when core commerce features are performed, and you can listen to and handle these events in asynchronous functions. You can think of Medusa's events like you'd think about webhooks in other commerce platforms, but instead of having to setup separate applications to handle webhooks, your efforts only go into writing the logic right in your Medusa codebase. + +You listen to an event in a subscriber, which is an asynchronous function that's executed when its associated event is emitted. + +![A diagram showcasing an example of how an event is emitted when an order is placed.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732277948/Medusa%20Book/order-placed-event-example_e4e4kw.jpg) + +Subscribers are useful to perform actions that aren't integral to the original flow. For example, you can handle the `order.placed` event in a subscriber that sends a confirmation email to the customer. The subscriber has no impact on the original order-placement flow, as it's executed outside of it. + +If the action you're performing is integral to the main flow of the core commerce feature, use [workflow hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) instead. + +### List of Emitted Events + +Find a list of all emitted events in [this reference](https://docs.medusajs.com/resources/events-reference/index.html.md). + +*** + +## How to Create a Subscriber? + +You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. The file exports the function to execute and the subscriber's configuration that indicate what event(s) it listens to. + +For example, create the file `src/subscribers/order-placed.ts` with the following content: + +![Example of subscriber file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866244/Medusa%20Book/subscriber-dir-overview_pusyeu.jpg) + +```ts title="src/subscribers/product-created.ts" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation" + +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const logger = container.resolve("logger") + + logger.info("Sending confirmation email...") + + await sendOrderConfirmationWorkflow(container) + .run({ + input: { + id: data.id, + }, + }) +} + +export const config: SubscriberConfig = { + event: `order.placed`, +} +``` + +This subscriber file exports: + +- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered. +- A configuration object with an `event` property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber. + +The subscriber function receives an object as a parameter that has the following properties: + +- `event`: An object with the event's details. The `data` property contains the data payload of the event emitted, which is the order's ID in this case. +- `container`: The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) that you can use to resolve registered resources. + +In the subscriber function, you use the container to resolve the Logger utility and log a message in the console. Also, assuming you have a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that sends an order confirmation email, you execute it in the subscriber. + +*** + +## Test the Subscriber + +To test the subscriber, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, try placing an order either using Medusa's API routes or the [Next.js Storefront](https://docs.medusajs.com/learn/storefront-development/nextjs-starter/index.html.md). You'll see the following message in the terminal: + +```bash +info: Processing order.placed which has 1 subscribers +Sending confirmation email... +``` + +The first message indicates that the `order.placed` event was emitted, and the second one is the message logged from the subscriber. + +*** + +## Event Module + +The subscription and emitting of events is handled by an Event Module, an architectural module that implements the pub/sub functionalities of Medusa's event system. + +Medusa provides two Event Modules out of the box: + +- [Local Event Module](https://docs.medusajs.com/resources/architectural-modules/event/local/index.html.md), used by default. It's useful for development, as you don't need additional setup to use it. +- [Redis Event Module](https://docs.medusajs.com/resources/architectural-modules/event/redis/index.html.md), which is useful in production. It uses [Redis](https://redis.io/) to implement Medusa's pub/sub events system. + +Medusa's [architecture](https://docs.medusajs.com/learn/introduction/architecture/index.html.md) also allows you to build a custom Event Module that uses a different service or logic to implement the pub/sub system. Learn how to build an Event Module in [this guide](https://docs.medusajs.com/resources/architectural-modules/event/create/index.html.md). + + +# Data Models Advanced Guides + +Data models are created and managed in a module. To learn how to create a data model in a custom module, refer to the [Modules chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +In the next chapters, you'll learn about defining data models in more details. 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. + + # Plugins In this chapter, you'll learn what a plugin is in Medusa. @@ -2454,129 +2583,35 @@ npx medusa db:migrate ``` -# Scheduled Jobs +# Modules -In this chapter, you’ll learn about scheduled jobs and how to use them. +In this chapter, you’ll learn about modules and how to create them. -## What is a Scheduled Job? +## What is a Module? -When building your commerce application, you may need to automate tasks and run them repeatedly at a specific schedule. For example, you need to automatically sync products to a third-party service once a day. +A module is a reusable package of functionalities related to a single domain or integration. Medusa comes with multiple pre-built modules for core commerce needs, such as the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) that holds the data models and business logic for cart operations. -In other commerce platforms, this feature isn't natively supported. Instead, you have to setup a separate application to execute cron jobs, which adds complexity as to how you expose this task to be executed in a cron job, or how do you debug it when it's not running within the platform's tooling. +When building a commerce application, you often need to introduce custom behavior specific to your products, 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 supporting this feature natively with scheduled jobs. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. Your efforts are only spent on implementing the functionality performed by the job, such as syncing products to an ERP. +Medusa removes this overhead by allowing you to easily write custom modules that integrate into the Medusa application without affecting the existing setup. You can also re-use your modules across Medusa projects. -- You want the action to execute at a specified schedule 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 when the application loads. Use [loaders](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md) instead. -- You want to execute the action if an event occurs. Use [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) instead. +As you learn more about Medusa, you will see that modules are central to customizations and integrations. With modules, your Medusa application can turn into a middleware solution for your commerce ecosystem. + +- You want to build a custom feature related to a single domain or integrate a third-party service. + +- You want to create a reusable package of customizations that include not only modules, but also API routes, workflows, and other customizations. Instead, use a [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). *** -## How to Create a Scheduled Job? +## How to Create a Module? -You create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. The file exports the asynchronous function to run, and the configurations indicating the schedule to run the function. +In a module, you define data models that represent new tables in the database, and you manage these models in a class called a service. Then, the Medusa application registers the module's service in the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) so that you can build commerce flows and features around the functionalities provided by the module. -For example, create the file `src/jobs/hello-world.ts` with the following content: +In this section, you'll build a Blog Module that has a `Post` data model and a service to manage that data model. You'll also expose an API endpoint to create a blog post. -![Example of scheduled job file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866423/Medusa%20Book/scheduled-job-dir-overview_ediqgm.jpg) +Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/blog`. -```ts title="src/jobs/hello-world.ts" highlights={highlights} -import { MedusaContainer } from "@medusajs/framework/types" - -export default async function greetingJob(container: MedusaContainer) { - const logger = container.resolve("logger") - - logger.info("Greeting!") -} - -export const config = { - name: "greeting-every-minute", - schedule: "* * * * *", -} -``` - -You export an asynchronous function that receives the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) as a parameter. In the function, you resolve the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) from the Medusa container and log a message. - -You also export a `config` object that has the following properties: - -- `name`: A unique name for the job. -- `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. - -This scheduled job executes every minute and logs into the terminal `Greeting!`. - -### Test the Scheduled Job - -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 -info: Greeting! -``` - -*** - -## Example: Sync Products Once a Day - -In this section, you'll find a brief example of how you use a scheduled job to sync products to a third-party service. - -When implementing flows spanning across systems or [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), you use [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). A workflow is a task made up of a series of steps, and you construct it like you would a regular function, but it's a special function that supports rollback mechanism in case of errors, background execution, and more. - -You can learn how to create a workflow in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), but this example assumes you already have a `syncProductToErpWorkflow` implemented. To execute this workflow once a day, create a scheduled job at `src/jobs/sync-products.ts` with the following content: - -```ts title="src/jobs/sync-products.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp" - -export default async function syncProductsJob(container: MedusaContainer) { - await syncProductToErpWorkflow(container) - .run() -} - -export const config = { - name: "sync-products-job", - schedule: "0 0 * * *", -} -``` - -In the scheduled job function, you execute the `syncProductToErpWorkflow` by invoking it and passing it the container, then invoking the `run` method. You also specify in the exported configurations the schedule `0 0 * * *` which indicates midnight time of every day. - -The next time you start the Medusa application, it will run this job every day at midnight. - - -# Modules - -In this chapter, you’ll learn about modules and how to create them. - -## What is a Module? - -A module is a reusable package of functionalities related to a single domain or integration. Medusa comes with multiple pre-built modules for core commerce needs, such as the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) that holds the data models and business logic for cart operations. - -When building a commerce application, you often need to introduce custom behavior specific to your products, 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 affecting the existing setup. You can also re-use your modules across Medusa projects. - -As you learn more about Medusa, you will see that modules are central to customizations and integrations. With modules, your Medusa application can turn into a middleware solution for your commerce ecosystem. - -- You want to build a custom feature related to a single domain or integrate a third-party service. - -- You want to create a reusable package of customizations that include not only modules, but also API routes, workflows, and other customizations. Instead, use a [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - -*** - -## How to Create a Module? - -In a module, you define data models that represent new tables in the database, and you manage these models in a class called a service. Then, the Medusa application registers the module's service in the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) so that you can build commerce flows and features around the functionalities provided by the module. - -In this section, you'll build a Blog Module that has a `Post` data model and a service to manage that data model. You'll also expose an API endpoint to create a blog post. - -Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/blog`. - -### 1. Create Data Model +### 1. Create Data Model A data model represents a table in the database. You create data models using Medusa's data modeling language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. @@ -2848,77 +2883,98 @@ This will create a post and return it in the response: You can also execute the workflow from a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) when an event occurs, or from a [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) to run it at a specified interval. -# Medusa's Architecture +# Scheduled Jobs -In this chapter, you'll learn about the architectural layers in Medusa. +In this chapter, you’ll learn about scheduled jobs and how to use them. -Find the full architectural diagram at the [end of this chapter](#full-diagram-of-medusas-architecture). +## What is a Scheduled Job? -## HTTP, Workflow, and Module Layers +When building your commerce application, you may need to automate tasks and run them repeatedly at a specific schedule. For example, you need to automatically sync products to a third-party service once a day. -Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes. +In other commerce platforms, this feature isn't natively supported. Instead, you have to setup a separate application to execute cron jobs, which adds complexity as to how you expose this task to be executed in a cron job, or how do you debug it when it's not running within the platform's tooling. -In a common Medusa application, requests go through four layers in the stack. In order of entry, those are: +Medusa removes this overhead by supporting this feature natively with scheduled jobs. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. Your efforts are only spent on implementing the functionality performed by the job, such as syncing products to an ERP. -1. API Routes (HTTP): Our API Routes are the typical entry point. The Medusa server is based on Express.js, which handles incoming requests. It can also connect to a Redis database that stores the server session data. -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. -4. Data store: Modules query the underlying datastore, which is a PostgreSQL database in common cases. +- You want the action to execute at a specified schedule 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 when the application loads. Use [loaders](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md) instead. +- You want to execute the action if an event occurs. Use [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) instead. -These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). +*** -![This diagram illustrates the entry point of requests into the Medusa application through API routes. It shows a storefront and an admin that can send a request to the HTTP layer. The HTTP layer then uses workflows to handle the business logic. Finally, the workflows use modules to query and manipulate data in the data stores.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) +## How to Create a Scheduled Job? -*** +You create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. The file exports the asynchronous function to run, and the configurations indicating the schedule to run the function. -## Database Layer +For example, create the file `src/jobs/hello-world.ts` with the following content: -The Medusa application injects into each module, including your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. +![Example of scheduled job file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866423/Medusa%20Book/scheduled-job-dir-overview_ediqgm.jpg) -Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). +```ts title="src/jobs/hello-world.ts" highlights={highlights} +import { MedusaContainer } from "@medusajs/framework/types" -![This diagram illustrates how modules connect to the database.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) +export default async function greetingJob(container: MedusaContainer) { + const logger = container.resolve("logger") -*** + logger.info("Greeting!") +} -## Third-Party Integrations Layer +export const config = { + name: "greeting-every-minute", + schedule: "* * * * *", +} +``` -Third-party services and systems are integrated through Medusa's Commerce and Architectural modules. You also create custom third-party integrations through a [custom module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +You export an asynchronous function that receives the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) as a parameter. In the function, you resolve the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) from the Medusa container and log a message. -Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). +You also export a `config` object that has the following properties: -### Commerce Modules +- `name`: A unique name for the job. +- `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. -[Commerce modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) integrate third-party services relevant for commerce or user-facing features. For example, you can integrate [Stripe](https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe/index.html.md) through a Payment Module Provider, or [ShipStation](https://docs.medusajs.com/resources/integrations/guides/shipstation/index.html.md) through a Fulfillment Module Provider. +This scheduled job executes every minute and logs into the terminal `Greeting!`. -You can also integrate third-party services for custom functionalities. For example, you can integrate [Sanity](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md) for rich CMS capabilities, or [Odoo](https://docs.medusajs.com/resources/recipes/erp/odoo/index.html.md) to sync your Medusa application with your ERP system. +### Test the Scheduled Job -You can replace any of the third-party services mentioned above to build your preferred commerce ecosystem. +To test out your scheduled job, start the Medusa application: -![Diagram illustrating the commerce modules integration to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175357/Medusa%20Book/service-commerce_qcbdsl.jpg) +```bash npm2yarn +npm run dev +``` -### Architectural Modules +After a minute, the following message will be logged to the terminal: -[Architectural modules](https://docs.medusajs.com/resources/architectural-modules/index.html.md) integrate third-party services and systems for architectural features. Medusa has the following Architectural modules: +```bash +info: Greeting! +``` -- [Cache Module](https://docs.medusajs.com/resources/architectural-modules/cache/index.html.md): Caches data that require heavy computation. You can integrate a custom module to handle the caching with services like Memcached, or use the existing [Redis Cache Module](https://docs.medusajs.com/resources/architectural-modules/cache/redis/index.html.md). -- [Event Module](https://docs.medusajs.com/resources/architectural-modules/event/index.html.md): A pub/sub system that allows you to subscribe to events and trigger them. You can integrate [Redis](https://docs.medusajs.com/resources/architectural-modules/event/redis/index.html.md) as the pub/sub system. -- [File Module](https://docs.medusajs.com/resources/architectural-modules/file/index.html.md): Manages file uploads and storage, such as upload of product images. You can integrate [AWS S3](https://docs.medusajs.com/resources/architectural-modules/file/s3/index.html.md) for file storage. -- [Locking Module](https://docs.medusajs.com/resources/architectural-modules/locking/index.html.md): Manages access to shared resources by multiple processes or threads, preventing conflict between processes and ensuring data consistency. You can integrate [Redis](https://docs.medusajs.com/resources/architectural-modules/locking/redis/index.html.md) for locking. -- [Notification Module](https://docs.medusajs.com/resources/architectural-modules/notification/index.html.md): Sends notifications to customers and users, such as for order updates or newsletters. You can integrate [SendGrid](https://docs.medusajs.com/resources/architectural-modules/notification/sendgrid/index.html.md) for sending emails. -- [Workflow Engine Module](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/index.html.md): Orchestrates workflows that hold the business logic of your application. You can integrate [Redis](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/redis/index.html.md) to orchestrate workflows. +*** -All of the third-party services mentioned above can be replaced to help you build your preferred architecture and ecosystem. +## Example: Sync Products Once a Day -![Diagram illustrating the architectural modules integration to third-party services and systems](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175342/Medusa%20Book/service-arch_ozvryw.jpg) +In this section, you'll find a brief example of how you use a scheduled job to sync products to a third-party service. -*** +When implementing flows spanning across systems or [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), you use [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). A workflow is a task made up of a series of steps, and you construct it like you would a regular function, but it's a special function that supports rollback mechanism in case of errors, background execution, and more. -## Full Diagram of Medusa's Architecture +You can learn how to create a workflow in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), but this example assumes you already have a `syncProductToErpWorkflow` implemented. To execute this workflow once a day, create a scheduled job at `src/jobs/sync-products.ts` with the following content: -The following diagram illustrates Medusa's architecture including all its layers. +```ts title="src/jobs/sync-products.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp" -![Full diagram illustrating Medusa's architecture combining all the different layers.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg) +export default async function syncProductsJob(container: MedusaContainer) { + await syncProductToErpWorkflow(container) + .run() +} + +export const config = { + name: "sync-products-job", + schedule: "0 0 * * *", +} +``` + +In the scheduled job function, you execute the `syncProductToErpWorkflow` by invoking it and passing it the container, then invoking the `run` method. You also specify in the exported configurations the schedule `0 0 * * *` which indicates midnight time of every day. + +The next time you start the Medusa application, it will run this job every day at midnight. # Configure Instrumentation @@ -3425,320 +3481,354 @@ You can now execute this workflow in a custom API route, scheduled job, or subsc Find a full list of the registered resources in the Medusa container and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). You can use these resources in your custom workflows. -# Medusa Testing Tools +# Worker Mode of Medusa Instance -In this chapter, you'll learn about Medusa's testing tools and how to install and configure them. +In this chapter, you'll learn about the different modes of running a Medusa instance and how to configure the mode. -## @medusajs/test-utils Package +## What is Worker Mode? -Medusa provides a Testing Framework to create integration tests for your custom API routes, modules, or other Medusa customizations. +By default, the Medusa application runs both the server, which handles all incoming requests, and the worker, which processes background tasks, in a single process. While this setup is suitable for development, it is not optimal for production environments where background tasks can be long-running or resource-intensive. -To use the Testing Framework, install `@medusajs/test-utils` as a `devDependency`: +In a production environment, you should deploy two separate instances of your Medusa application: -```bash npm2yarn -npm install --save-dev @medusajs/test-utils@latest -``` +1. A server instance that handles incoming requests to the application's API routes. +2. A worker instance that processes background tasks. This includes scheduled jobs and subscribers. + +You don't need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables, as you'll see later in this chapter. + +This separation ensures that the server instance remains responsive to incoming requests, while the worker instance processes tasks in the background. + +![Diagram showcasing how the server and worker work together](https://res.cloudinary.com/dza7lstvk/image/upload/fl_lossy/f_auto/r_16/ar_16:9,c_pad/v1/Medusa%20Book/medusa-worker_klkbch.jpg?_a=BATFJtAA0) *** -## Install and Configure Jest +## How to Set Worker Mode -Writing tests with `@medusajs/test-utils`'s tools requires installing and configuring Jest in your project. +You can set the worker mode of your application using the `projectConfig.workerMode` configuration in the `medusa-config.ts`. The `workerMode` configuration accepts the following values: -Run the following command to install the required Jest dependencies: +- `shared`: (default) run the application in a single process, meaning the worker and server run in the same process. +- `worker`: run a worker process only. +- `server`: run the application server only. -```bash npm2yarn -npm install --save-dev jest @types/jest @swc/jest +Instead of creating different projects with different worker mode configurations, you can set the worker mode using an environment variable. Then, the worker mode configuration will change based on the environment variable. + +For example, set the worker mode in `medusa-config.ts` to the following: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + workerMode: process.env.WORKER_MODE || "shared", + // ... + }, + // ... +}) ``` -Then, create the file `jest.config.js` with the following content: +You set the worker mode configuration to the `process.env.WORKER_MODE` environment variable and set a default value of `shared`. -```js title="jest.config.js" -const { loadEnv } = require("@medusajs/framework/utils") -loadEnv("test", process.cwd()) +Then, in the deployed server Medusa instance, set `WORKER_MODE` to `server`, and in the worker Medusa instance, set `WORKER_MODE` to `worker`: -module.exports = { - transform: { - "^.+\\.[jt]s$": [ - "@swc/jest", - { - jsc: { - parser: { syntax: "typescript", decorators: true }, - }, - }, - ], - }, - testEnvironment: "node", - moduleFileExtensions: ["js", "ts", "json"], - modulePathIgnorePatterns: ["dist/"], - setupFiles: ["./integration-tests/setup.js"], -} +### Server Medusa Instance -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"] -} +```bash +WORKER_MODE=server ``` -Next, create the `integration-tests/setup.js` file with the following content: - -```js title="integration-tests/setup.js" -const { MetadataStorage } = require("@mikro-orm/core") +### Worker Medusa Instance -MetadataStorage.clear() +```bash +WORKER_MODE=worker ``` -*** +### Disable Admin in Worker Mode -## Add Test Commands +Since the worker instance only processes background tasks, you should disable the admin interface in it. That will save resources in the worker instance. -Finally, add the following scripts to `package.json`: +To disable the admin interface, set the `admin.disable` configuration in the `medusa-config.ts` file: -```json title="package.json" -"scripts": { +```ts title="medusa-config.ts" +module.exports = defineConfig({ + admin: { + disable: process.env.ADMIN_DISABLED === "true" || + false, + }, // ... - "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. - -Medusa's Testing Framework works for integration tests only. You can write unit tests using Jest. +Similar to before, you set the value in an environment variable, allowing you to enable or disable the admin interface based on the environment. -*** +Then, in the deployed server Medusa instance, set `ADMIN_DISABLED` to `false`, and in the worker Medusa instance, set `ADMIN_DISABLED` to `true`: -## Test Tools and Writing Tests +### Server Medusa Instance -The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests. +```bash +ADMIN_DISABLED=false +``` +### Worker Medusa Instance -# Customize Medusa Admin Dashboard +```bash +ADMIN_DISABLED=true +``` -In the previous chapters, you've customized your Medusa application to [add brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), [expose an API route to create brands](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), and [linked brands to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md). -After customizing and extending your application with new features, you may need to provide an interface for admin users to utilize these features. The Medusa Admin dashboard is extendable, allowing you to: +# Medusa's Architecture -- Insert components, called [widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md), on existing pages. -- Add new pages, called [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). +In this chapter, you'll learn about the architectural layers in Medusa. -From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard +Find the full architectural diagram at the [end of this chapter](#full-diagram-of-medusas-architecture). -*** +## HTTP, Workflow, and Module Layers -## Next Chapters: View Brands in Dashboard +Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes. -In the next chapters, you'll continue with the brands example to: +In a common Medusa application, requests go through four layers in the stack. In order of entry, those are: -- Add a new section to the product details page that shows the product's brand. -- Add a new page in the dashboard that shows all brands in the store. +1. API Routes (HTTP): Our API Routes are the typical entry point. The Medusa server is based on Express.js, which handles incoming requests. It can also connect to a Redis database that stores the server session data. +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. +4. Data store: Modules query the underlying datastore, which is a PostgreSQL database in common cases. +These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -# Build Custom Features +![This diagram illustrates the entry point of requests into the Medusa application through API routes. It shows a storefront and an admin that can send a request to the HTTP layer. The HTTP layer then uses workflows to handle the business logic. Finally, the workflows use modules to query and manipulate data in the data stores.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) -In the upcoming chapters, you'll follow step-by-step guides to build custom features in Medusa. These guides gradually introduce Medusa's concepts to help you understand what they are and how to use them. +*** -By following these guides, you'll add brands to the Medusa application that you can associate with products. +## Database Layer -To build a custom feature in Medusa, you need three main tools: +The Medusa application injects into each module, including your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. -- [Module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md): a package with commerce logic for a single domain. It defines new tables to add to the database, and a class of methods to manage these tables. -- [Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md): a tool to perform an operation comprising multiple steps with built-in rollback and retry mechanisms. -- [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md): a REST endpoint that exposes commerce features to clients, such as the admin dashboard or a storefront. The API route executes a workflow that implements the commerce feature using modules. +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -![Diagram showcasing the flow of a custom developed feature](https://res.cloudinary.com/dza7lstvk/image/upload/v1725867628/Medusa%20Book/custom-development_nofvp6.jpg) +![This diagram illustrates how modules connect to the database.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) *** -## Next Chapters: Brand Module Example - -The next chapters will guide you to: - -1. Build 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. +## Third-Party Integrations Layer +Third-party services and systems are integrated through Medusa's Commerce and Architectural modules. You also create custom third-party integrations through a [custom module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -# Integrate Third-Party Systems +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -Commerce applications often connect to third-party systems that provide additional or specialized features. For example, you may integrate a Content-Management System (CMS) for rich content features, a payment provider to process credit-card payments, and a notification service to send emails. +### Commerce Modules -Medusa's framework facilitates integrating these systems and orchestrating operations across them, saving you the effort of managing them yourself. You won't find those capabilities in other commerce platforms that in these scenarios become a bottleneck to building customizations and iterating quickly. +[Commerce modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) integrate third-party services relevant for commerce or user-facing features. For example, you can integrate [Stripe](https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe/index.html.md) through a Payment Module Provider, or [ShipStation](https://docs.medusajs.com/resources/integrations/guides/shipstation/index.html.md) through a Fulfillment Module Provider. -In Medusa, you integrate a third-party system by: +You can also integrate third-party services for custom functionalities. For example, you can integrate [Sanity](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md) for rich CMS capabilities, or [Odoo](https://docs.medusajs.com/resources/recipes/erp/odoo/index.html.md) to sync your Medusa application with your ERP system. -1. Creating a module whose service provides the methods to connect to and perform operations in the third-party system. -2. Building workflows that complete tasks spanning across systems. You use the module that integrates a third-party system in the workflow's steps. -3. Executing the workflows you built in an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), at a scheduled time, or when an event is emitted. +You can replace any of the third-party services mentioned above to build your preferred commerce ecosystem. -*** +![Diagram illustrating the commerce modules integration to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175357/Medusa%20Book/service-commerce_qcbdsl.jpg) -## Next Chapters: Sync Brands Example +### Architectural Modules -In the previous chapters, you've [added brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to your Medusa application. In the next chapters, you will: +[Architectural modules](https://docs.medusajs.com/resources/architectural-modules/index.html.md) integrate third-party services and systems for architectural features. Medusa has the following Architectural modules: -1. Integrate a dummy third-party CMS in the Brand Module. -2. Sync brands to the CMS when a brand is created. -3. Sync brands from the CMS at a daily schedule. +- [Cache Module](https://docs.medusajs.com/resources/architectural-modules/cache/index.html.md): Caches data that require heavy computation. You can integrate a custom module to handle the caching with services like Memcached, or use the existing [Redis Cache Module](https://docs.medusajs.com/resources/architectural-modules/cache/redis/index.html.md). +- [Event Module](https://docs.medusajs.com/resources/architectural-modules/event/index.html.md): A pub/sub system that allows you to subscribe to events and trigger them. You can integrate [Redis](https://docs.medusajs.com/resources/architectural-modules/event/redis/index.html.md) as the pub/sub system. +- [File Module](https://docs.medusajs.com/resources/architectural-modules/file/index.html.md): Manages file uploads and storage, such as upload of product images. You can integrate [AWS S3](https://docs.medusajs.com/resources/architectural-modules/file/s3/index.html.md) for file storage. +- [Locking Module](https://docs.medusajs.com/resources/architectural-modules/locking/index.html.md): Manages access to shared resources by multiple processes or threads, preventing conflict between processes and ensuring data consistency. You can integrate [Redis](https://docs.medusajs.com/resources/architectural-modules/locking/redis/index.html.md) for locking. +- [Notification Module](https://docs.medusajs.com/resources/architectural-modules/notification/index.html.md): Sends notifications to customers and users, such as for order updates or newsletters. You can integrate [SendGrid](https://docs.medusajs.com/resources/architectural-modules/notification/sendgrid/index.html.md) for sending emails. +- [Workflow Engine Module](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/index.html.md): Orchestrates workflows that hold the business logic of your application. You can integrate [Redis](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/redis/index.html.md) to orchestrate workflows. +All of the third-party services mentioned above can be replaced to help you build your preferred architecture and ecosystem. -# Extend Core Commerce Features +![Diagram illustrating the architectural modules integration to third-party services and systems](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175342/Medusa%20Book/service-arch_ozvryw.jpg) -In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features. +*** -In other commerce platforms, you extend core features and models through hacky workarounds that can introduce unexpected issues and side effects across the platform. It also makes your application difficult to maintain and upgrade in the long run. +## Full Diagram of Medusa's Architecture -Medusa's framework and orchestration tools mitigate these issues while supporting all your customization needs: +The following diagram illustrates Medusa's architecture including all its layers. -- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md): Link data models of different modules without building direct dependencies, ensuring that the Medusa application integrates your modules without side effects. -- [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md): inject custom functionalities into a workflow at predefined points, called hooks. This allows you to perform custom actions as a part of a core workflow without hacky workarounds. -- [Additional Data in API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md): Configure core API routes to accept request parameters relevant to your customizations. These parameters are passed to the underlying workflow's hooks, where you can manage your custom data as part of an existing flow. +![Full diagram illustrating Medusa's architecture combining all the different layers.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg) -*** -## Next Chapters: Link Brands to Products Example +# Medusa Testing Tools -The next chapters explain how to use the tools mentioned above with step-by-step guides. You'll continue with the [brands example from the previous chapters](https://docs.medusajs.com/learn/customization/custom-features/index.html.md) to: +In this chapter, you'll learn about Medusa's testing tools and how to install and configure them. -- Link brands from the custom [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to products from Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md). -- Extend the core product-creation workflow and the API route that uses it to allow setting the brand of a newly created product. -- Retrieve a product's associated brand's details. +## @medusajs/test-utils Package +Medusa provides a Testing Framework to create integration tests for your custom API routes, modules, or other Medusa customizations. -# Customizations Next Steps: Learn the Fundamentals +To use the Testing Framework, install `@medusajs/test-utils` as a `devDependency`: -The previous guides introduced Medusa's different concepts and how you can use them to customize Medusa for a realistic use case, You added brands to your application, linked them to products, customized the admin dashboard, and integrated a third-party CMS. +```bash npm2yarn +npm install --save-dev @medusajs/test-utils@latest +``` -The next chapters will cover each of these concepts in depth, with the different ways you can use them, their options or configurations, and more advanced features that weren't covered in the previous guides. While you can start building with Medusa, it's highly recommended to follow the next chapters for a better understanding of Medusa's fundamentals. +*** -## Useful Guides +## Install and Configure Jest -The following guides and references are useful for your development journey: +Writing tests with `@medusajs/test-utils`'s tools requires installing and configuring Jest in your project. -3. [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md): Browse the list of commerce modules in Medusa and their references to learn how to use them. -4. [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md): Learn about the methods generated by `MedusaService` with examples. -5. [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md): Browse the list of core workflows and their hooks that are useful for your customizations. -6. [Admin Injection Zones](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md): Browse the injection zones in the Medusa Admin to learn where you can inject widgets. +Run the following command to install the required Jest dependencies: -*** +```bash npm2yarn +npm install --save-dev jest @types/jest @swc/jest +``` -## More Examples in Recipes +Then, create the file `jest.config.js` with the following content: -In the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more. +```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/"], + setupFiles: ["./integration-tests/setup.js"], +} -# Re-Use Customizations with Plugins +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"] +} +``` -In the previous chapters, you've learned important concepts related to creating modules, implementing commerce features in workflows, exposing those features in API routes, customizing the Medusa Admin dashboard with Admin Extensions, and integrating third-party systems. +Next, create the `integration-tests/setup.js` file with the following content: -You've implemented the brands example within a single Medusa application. However, this approach is not scalable when you want to reuse your customizations across multiple projects. +```js title="integration-tests/setup.js" +const { MetadataStorage } = require("@mikro-orm/core") -To reuse your customizations across multiple Medusa applications, such as implementing brands in different projects, you can create a plugin. A plugin is an NPM package that encapsulates your customizations and can be installed in any Medusa application. Plugins can include modules, workflows, API routes, Admin Extensions, and more. +MetadataStorage.clear() +``` -![Diagram showcasing how the Brand Plugin would add its resources to any application it's installed in](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540091/Medusa%20Book/brand-plugin_bk9zi9.jpg) +*** -Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM. +## Add Test Commands -To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). +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" +}, +``` -# Worker Mode of Medusa Instance +You now have two commands: -In this chapter, you'll learn about the different modes of running a Medusa instance and how to configure the mode. +- `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. -## What is Worker Mode? +Medusa's Testing Framework works for integration tests only. You can write unit tests using Jest. -By default, the Medusa application runs both the server, which handles all incoming requests, and the worker, which processes background tasks, in a single process. While this setup is suitable for development, it is not optimal for production environments where background tasks can be long-running or resource-intensive. +*** -In a production environment, you should deploy two separate instances of your Medusa application: +## Test Tools and Writing Tests -1. A server instance that handles incoming requests to the application's API routes. -2. A worker instance that processes background tasks. This includes scheduled jobs and subscribers. +The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests. -You don't need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables, as you'll see later in this chapter. -This separation ensures that the server instance remains responsive to incoming requests, while the worker instance processes tasks in the background. +# Usage Information -![Diagram showcasing how the server and worker work together](https://res.cloudinary.com/dza7lstvk/image/upload/fl_lossy/f_auto/r_16/ar_16:9,c_pad/v1/Medusa%20Book/medusa-worker_klkbch.jpg?_a=BATFJtAA0) +At Medusa, we strive to provide the best experience for developers using our platform. For that reason, Medusa collects anonymous and non-sensitive data that provides a global understanding of how users are using Medusa. *** -## How to Set Worker Mode +## Purpose -You can set the worker mode of your application using the `projectConfig.workerMode` configuration in the `medusa-config.ts`. The `workerMode` configuration accepts the following values: +As an open source solution, we work closely and constantly interact with our community to ensure that we provide the best experience for everyone using Medusa. -- `shared`: (default) run the application in a single process, meaning the worker and server run in the same process. -- `worker`: run a worker process only. -- `server`: run the application server only. +We are capable of getting a general understanding of how developers use Medusa and what general issues they run into through different means such as our Discord server, GitHub issues and discussions, and occasional one-on-one sessions. -Instead of creating different projects with different worker mode configurations, you can set the worker mode using an environment variable. Then, the worker mode configuration will change based on the environment variable. +However, although these methods can be insightful, they’re not enough to get a full and global understanding of how developers are using Medusa, especially in production. -For example, set the worker mode in `medusa-config.ts` to the following: +Collecting this data allows us to understand certain details such as: -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - workerMode: process.env.WORKER_MODE || "shared", - // ... - }, - // ... -}) -``` +- What operating system do most Medusa developers use? +- What version of Medusa is widely used? +- What parts of the Medusa Admin are generally undiscovered by our users? +- How much data do users manage through our Medusa Admin? Is it being used for large number of products, orders, and other types of data? +- What Node version is globally used? Should we focus our efforts on providing support for versions that we don’t currently support? -You set the worker mode configuration to the `process.env.WORKER_MODE` environment variable and set a default value of `shared`. +*** -Then, in the deployed server Medusa instance, set `WORKER_MODE` to `server`, and in the worker Medusa instance, set `WORKER_MODE` to `worker`: +## Medusa Application Analytics -### Server Medusa Instance +This section covers which data in the Medusa application are collected and how to opt out of it. + +### Collected Data in the Medusa Application + +The following data is being collected on your Medusa application: + +- Unique project ID generated with UUID. +- Unique machine ID generated with UUID. +- Operating system information including Node version or operating system platform used. +- The version of the Medusa application and Medusa CLI are used. + +Data is only collected when the Medusa application is run with the command `medusa start`. + +### How to Opt Out + +If you prefer to disable data collection, you can do it either by setting the following environment variable to true: ```bash -WORKER_MODE=server +MEDUSA_DISABLE_TELEMETRY=true ``` -### Worker Medusa Instance +Or, you can run the following command in the root of your Medusa application project to disable it: ```bash -WORKER_MODE=worker +npx medusa telemetry --disable ``` -### Disable Admin in Worker Mode +*** -Since the worker instance only processes background tasks, you should disable the admin interface in it. That will save resources in the worker instance. +## Admin Analytics -To disable the admin interface, set the `admin.disable` configuration in the `medusa-config.ts` file: +This section covers which data in the admin are collected and how to opt out of it. -```ts title="medusa-config.ts" -module.exports = defineConfig({ - admin: { - disable: process.env.ADMIN_DISABLED === "true" || - false, - }, - // ... -}) -``` +### Collected Data in Admin -Similar to before, you set the value in an environment variable, allowing you to enable or disable the admin interface based on the environment. +Users have the option to [enable or disable the anonymization](#how-to-enable-anonymization) of the collected data. -Then, in the deployed server Medusa instance, set `ADMIN_DISABLED` to `false`, and in the worker Medusa instance, set `ADMIN_DISABLED` to `true`: +The following data is being collected on your admin: -### Server Medusa Instance +- The name of the store. +- The email of the user. +- The total number of products, orders, discounts, and users. +- The number of regions and their names. +- The currencies used in the store. +- Errors that occur while using the admin. -```bash -ADMIN_DISABLED=false -``` +### How to Enable Anonymization -### Worker Medusa Instance +To enable anonymization of your data from the Medusa Admin: + +1. Go to Settings → Personal Information. +2. In the Usage insights section, click on the “Edit preferences” button. +3. Enable the "Anonymize my usage data” toggle. +4. Click on the “Submit and close” button. + +### How to Opt-Out + +To opt out of analytics collection in the Medusa Admin, set the following environment variable: ```bash -ADMIN_DISABLED=true +MEDUSA_FF_ANALYTICS=false ``` @@ -3808,11411 +3898,11214 @@ The Next.js Starter is compatible with some Medusa integrations out-of-the-box, Refer to the [Next.js Starter reference](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) for more details. -# Usage Information - -At Medusa, we strive to provide the best experience for developers using our platform. For that reason, Medusa collects anonymous and non-sensitive data that provides a global understanding of how users are using Medusa. - -*** - -## Purpose - -As an open source solution, we work closely and constantly interact with our community to ensure that we provide the best experience for everyone using Medusa. +# Guide: Create Brand API Route -We are capable of getting a general understanding of how developers use Medusa and what general issues they run into through different means such as our Discord server, GitHub issues and discussions, and occasional one-on-one sessions. +In the previous two chapters, you created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that added the concepts of brands to your application, then created a [workflow to create a brand](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). In this chapter, you'll expose an API route that allows admin users to create a brand using the workflow from the previous chapter. -However, although these methods can be insightful, they’re not enough to get a full and global understanding of how developers are using Medusa, especially in production. +An API Route is an endpoint that acts as an entry point for other clients to interact with your Medusa customizations, such as the admin dashboard, storefronts, or third-party systems. -Collecting this data allows us to understand certain details such as: +The Medusa core application provides a set of [admin](https://docs.medusajs.com/api/admin) and [store](https://docs.medusajs.com/api/store) API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities. -- What operating system do most Medusa developers use? -- What version of Medusa is widely used? -- What parts of the Medusa Admin are generally undiscovered by our users? -- How much data do users manage through our Medusa Admin? Is it being used for large number of products, orders, and other types of data? -- What Node version is globally used? Should we focus our efforts on providing support for versions that we don’t currently support? +### Prerequisites -*** +- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) -## Medusa Application Analytics +## 1. Create the API Route -This section covers which data in the Medusa application are collected and how to opt out of it. +You create an API route in a `route.{ts,js}` file under a sub-directory of the `src/api` directory. The file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). -### Collected Data in the Medusa Application +Learn more about API routes [in this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md). -The following data is being collected on your Medusa application: +The route's path is the path of `route.{ts,js}` relative to `src/api`. So, to create the API route at `/admin/brands`, create the file `src/api/admin/brands/route.ts` with the following content: -- Unique project ID generated with UUID. -- Unique machine ID generated with UUID. -- Operating system information including Node version or operating system platform used. -- The version of the Medusa application and Medusa CLI are used. +![Directory structure of the Medusa application after adding the route](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869882/Medusa%20Book/brand-route-dir-overview-2_hjqlnf.jpg) -Data is only collected when the Medusa application is run with the command `medusa start`. +```ts title="src/api/admin/brands/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + createBrandWorkflow, +} from "../../../workflows/create-brand" -### How to Opt Out +type PostAdminCreateBrandType = { + name: string +} -If you prefer to disable data collection, you can do it either by setting the following environment variable to true: +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { result } = await createBrandWorkflow(req.scope) + .run({ + input: req.validatedBody, + }) -```bash -MEDUSA_DISABLE_TELEMETRY=true + res.json({ brand: result }) +} ``` -Or, you can run the following command in the root of your Medusa application project to disable it: - -```bash -npx medusa telemetry --disable -``` +You export a route handler function with its name (`POST`) being the HTTP method of the API route you're exposing. -*** +The function receives two parameters: a `MedusaRequest` object to access request details, and `MedusaResponse` object to return or manipulate the response. The `MedusaRequest` object's `scope` property is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) that holds framework tools and custom and core modules' services. -## Admin Analytics +`MedusaRequest` accepts the request body's type as a type argument. -This section covers which data in the admin are collected and how to opt out of it. +In the API route's handler, you execute the `createBrandWorkflow` by invoking it and passing the Medusa container `req.scope` as a parameter, then invoking its `run` method. You pass the workflow's input in the `input` property of the `run` method's parameter. You pass the request body's parameters using the `validatedBody` property of `MedusaRequest`. -### Collected Data in Admin +You return a JSON response with the created brand using the `res.json` method. -Users have the option to [enable or disable the anonymization](#how-to-enable-anonymization) of the collected data. +*** -The following data is being collected on your admin: +## 2. Create Validation Schema -- The name of the store. -- The email of the user. -- The total number of products, orders, discounts, and users. -- The number of regions and their names. -- The currencies used in the store. -- Errors that occur while using the admin. +The API route you created accepts the brand's name in the request body. So, you'll create a schema used to validate incoming request body parameters. -### How to Enable Anonymization +Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. -To enable anonymization of your data from the Medusa Admin: +Learn more about API route validation in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). -1. Go to Settings → Personal Information. -2. In the Usage insights section, click on the “Edit preferences” button. -3. Enable the "Anonymize my usage data” toggle. -4. Click on the “Submit and close” button. +You create a validation schema in a TypeScript or JavaScript file under a sub-directory of the `src/api` directory. So, create the file `src/api/admin/brands/validators.ts` with the following content: -### How to Opt-Out +![Directory structure of Medusa application after adding validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869806/Medusa%20Book/brand-route-dir-overview-1_yfyjss.jpg) -To opt out of analytics collection in the Medusa Admin, set the following environment variable: +```ts title="src/api/admin/brands/validators.ts" +import { z } from "zod" -```bash -MEDUSA_FF_ANALYTICS=false +export const PostAdminCreateBrand = z.object({ + name: z.string(), +}) ``` +You export a validation schema that expects in the request body an object having a `name` property whose value is a string. -# Admin Development Constraints - -This chapter lists some constraints of admin widgets and UI routes. - -## Arrow Functions +You can then replace `PostAdminCreateBrandType` in `src/api/admin/brands/route.ts` with the following: -Widget and UI route components must be created as arrow functions. +```ts title="src/api/admin/brands/route.ts" +// ... +import { z } from "zod" +import { PostAdminCreateBrand } from "./validators" -```ts highlights={arrowHighlights} -// Don't -function ProductWidget() { - // ... -} +type PostAdminCreateBrandType = z.infer -// 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. - -```ts highlights={zoneHighlights} -// Don't -export const config = defineWidgetConfig({ - zone: `product.details.before`, -}) +## 3. Add Validation Middleware -// Don't -const ZONE = "product.details.after" -export const config = defineWidgetConfig({ - zone: ZONE, -}) +A middleware is a function executed before the route handler when a request is sent to an API Route. It's useful to guard API routes, parse custom request body types, and apply validation on an API route. -// Do -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) -``` +Learn more about middlewares in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). +Medusa provides a `validateAndTransformBody` middleware that accepts a Zod validation schema and returns a response error if a request is sent with body parameters that don't satisfy the validation schema. -# Environment Variables in Admin Customizations +Middlewares are defined in the special file `src/api/middlewares.ts`. So, to add the validation middleware on the API route you created in the previous step, create the file `src/api/middlewares.ts` with the following content: -In this chapter, you'll learn how to use environment variables in your admin customizations. +![Directory structure of the Medusa application after adding the middleware](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869977/Medusa%20Book/brand-route-dir-overview-3_kcx511.jpg) -To learn how envirnment variables are generally loaded in Medusa based on your application's environment, check out [this chapter](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md). +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformBody, +} from "@medusajs/framework/http" +import { PostAdminCreateBrand } from "./admin/brands/validators" -## How to Set Environment Variables +export default defineMiddlewares({ + routes: [ + { + matcher: "/admin/brands", + method: "POST", + middlewares: [ + validateAndTransformBody(PostAdminCreateBrand), + ], + }, + ], +}) +``` -The Medusa Admin is built on top of [Vite](https://vite.dev/). To set an environment variable that you want to use in a widget or UI route, prefix the environment variable with `VITE_`. +You define the middlewares using the `defineMiddlewares` function and export its returned value. The function accepts an object having a `routes` property, which is an array of middleware objects. -For example: +In the middleware object, you define three properties: -```plain -VITE_MY_API_KEY=sk_123 -``` +- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. You pass the create brand's route `/admin/brand`. +- `method`: The HTTP method to restrict the middleware to, which is `POST`. +- `middlewares`: An array of middlewares to apply on the route. You pass the `validateAndTransformBody` middleware, passing it the Zod schema you created earlier. -*** +The Medusa application will now validate the body parameters of `POST` requests sent to `/admin/brands` to ensure they match the Zod validation schema. If not, an error is returned in the response specifying the issues to fix in the request body. -## How to Use Environment Variables +*** -To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object. +## Test API Route -For example: +To test out the API route, start the Medusa application with the following command: -```tsx highlights={[["8"]]} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" +```bash npm2yarn +npm run dev +``` -const ProductWidget = () => { - return ( - -
- API Key: {import.meta.env.VITE_MY_API_KEY} -
-
- ) -} +Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) +So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route: -export default ProductWidget +```bash +curl -X POST 'http://localhost:9000/auth/user/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "admin@medusa-test.com", + "password": "supersecret" +}' ``` -In this example, you display the API key in a widget using `import.meta.env.VITE_MY_API_KEY`. +Make sure to replace the email and password with your admin user's credentials. -### Type Error on import.meta.env +Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). -If you receive a type error on `import.meta.env`, create the file `src/admin/vite-env.d.ts` with the following content: +Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: -```ts title="src/admin/vite-env.d.ts" -/// +```bash +curl -X POST 'http://localhost:9000/admin/brands' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "name": "Acme" +}' ``` -This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables. +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" + } +} +``` *** -## Check Node Environment in Admin Customizations +## Summary -To check the current environment, Vite exposes two variables: +By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand. You did that by: -- `import.meta.env.DEV`: Returns `true` if the current environment is development. -- `import.meta.env.PROD`: Returns `true` if the current environment is production. +1. Creating a module that defines and manages a `brand` table in the database. +2. Creating a workflow that uses the module's 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. -Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode). +*** +## Next Steps: Associate Brand with Product -# Admin Development Tips +Now that you have brands in your Medusa application, you want to associate a brand with a product, which is defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md). -In this chapter, you'll find some tips for your admin development. +In the next chapters, you'll learn how to build associations between data models defined in different modules. -## Send Requests to API Routes -To send a request to an API route in the Medusa Application, use Medusa's [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) with [Tanstack Query](https://tanstack.com/query/latest). Both of these tools are installed in your project by default. +# Guide: Implement Brand Module -Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. +In this chapter, you'll build a Brand Module that adds a `brand` table to the database and provides data-management features for it. -First, create the file `src/admin/lib/config.ts` to setup the SDK for use in your customizations: +A module is a reusable package of functionalities related to a single domain or integration. Medusa comes with multiple pre-built modules for core commerce needs, such as the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) that holds the data models and business logic for cart operations. -```ts -import Medusa from "@medusajs/js-sdk" +In a module, you create data models and business logic to manage them. In the next chapters, you'll see how you use the module to build commerce features. -export const sdk = new Medusa({ - baseUrl: import.meta.env.VITE_BACKEND_URL || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", - }, -}) -``` +Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -Notice that you use `import.meta.env` to access environment variables in your customizations, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). +## 1. Create Module Directory -Learn more about the JS SDK's configurations [this documentation](https://docs.medusajs.com/resources/js-sdk#js-sdk-configurations/index.html.md). +Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/brand` that will hold the Brand Module's files. -Then, use the configured SDK with the `useQuery` Tanstack Query hook to send `GET` requests, and `useMutation` hook to send `POST` or `DELETE` requests. +![Directory structure in Medusa project after adding the brand directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868844/Medusa%20Book/brand-dir-overview-1_hxwvgx.jpg) -For example: +*** -### Query +## 2. Create Data Model -```tsx title="src/admin/widgets/product-widget.ts" highlights={queryHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Button, Container } from "@medusajs/ui" -import { useQuery } from "@tanstack/react-query" -import { sdk } from "../lib/config" -import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" +A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. -const ProductWidget = () => { - const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list(), - queryKey: ["products"], - }) - - return ( - - {isLoading && Loading...} - {data?.products && ( -
    - {data.products.map((product) => ( -
  • {product.title}
  • - ))} -
- )} -
- ) -} +Learn more about data models in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#1-create-data-model/index.html.md). -export const config = defineWidgetConfig({ - zone: "product.list.before", -}) +You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, 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: -export default ProductWidget +![Directory structure in module after adding the brand data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868920/Medusa%20Book/brand-dir-overview-2_lexhdl.jpg) + +```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(), +}) ``` -### Mutation +You create a `Brand` data model which has an `id` primary key property, and a `name` text property. -```tsx title="src/admin/widgets/product-widget.ts" highlights={mutationHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Button, Container } from "@medusajs/ui" -import { useMutation } from "@tanstack/react-query" -import { sdk } from "../lib/config" -import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" +You define the data model using the `define` method of the DML. It accepts two parameters: -const ProductWidget = ({ - data: productData, -}: DetailWidgetProps) => { - const { mutateAsync } = useMutation({ - mutationFn: (payload: HttpTypes.AdminUpdateProduct) => - sdk.admin.product.update(productData.id, payload), - onSuccess: () => alert("updated product"), - }) +1. The first one is the name of the data model's table in the database. Use snake-case names. +2. The second is an object, which is the data model's schema. - const handleUpdate = () => { - mutateAsync({ - title: "New Product Title", - }) - } - - return ( - - - - ) -} +Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/property-types/index.html.md). -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) +*** -export default ProductWidget -``` +## 3. Create Module Service -You can also send requests to custom routes as explained in the [JS SDK reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). +You perform database operations on your data models in a service, which is a class exported by the module and acts like an interface to its functionalities. -### Use Route Loaders for Initial Data +In this step, you'll create the Brand Module's service that provides methods to manage the `Brand` data model. In the next chapters, you'll use this service when exposing custom features that involve managing brands. -You may need to retrieve data before your component is rendered, or you may need to pass some initial data to your component to be used while data is being fetched. In those cases, you can use a [route loader](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md). +Learn more about services in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#2-create-service/index.html.md). -*** +You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, create the file `src/modules/brand/service.ts` with the following content: -## Global Variables in Admin Customizations +![Directory structure in module after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868984/Medusa%20Book/brand-dir-overview-3_jo7baj.jpg) -In your admin customizations, you can use the following global variables: +```ts title="src/modules/brand/service.ts" highlights={serviceHighlights} +import { MedusaService } from "@medusajs/framework/utils" +import { Brand } from "./models/brand" -- `__BASE__`: The base path of the Medusa Admin, as set in the [admin.path](https://docs.medusajs.com/learn/configurations/medusa-config#path/index.html.md) configuration in `medusa-config.ts`. -- `__BACKEND_URL__`: The URL to the Medusa backend, as set in the [admin.backendUrl](https://docs.medusajs.com/learn/configurations/medusa-config#backendurl/index.html.md) configuration in `medusa-config.ts`. -- `__STOREFRONT_URL__`: The URL to the storefront, as set in the [admin.storefrontUrl](https://docs.medusajs.com/learn/configurations/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`. +class BrandModuleService extends MedusaService({ + Brand, +}) { -*** +} -## Admin Translations +export default BrandModuleService +``` -The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions. +The `BrandModuleService` extends a class returned by `MedusaService` from the Modules SDK. This function generates a class with data-management methods for your module's data models. -Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/learn/resources/contribution-guidelines/admin-translations/index.html.md). +The `MedusaService` function receives an object of the module's data models as a parameter, and generates methods to manage those data models. So, the `BrandModuleService` now has methods like `createBrands` and `retrieveBrand` to manage the `Brand` data model. +You'll use these methods in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). -# Admin Routing Customizations +Find a reference of all generated methods in [this guide](https://docs.medusajs.com/resources/service-factory-reference/index.html.md). -The Medusa Admin dashboard uses [React Router](https://reactrouter.com) under the hood to manage routing. So, you can have more flexibility in routing-related customizations using some of React Router's utilities, hooks, and components. +*** -In this chapter, you'll learn about routing-related customizations that you can use in your admin customizations using React Router. +## 4. Export Module Definition -`react-router-dom` is available in your project by default through the Medusa packages. You don't need to install it separately. +A module must export a definition that tells Medusa the name of the module and its main service. This definition is exported in an `index.ts` file at the module's root directory. -## Link to a Page +So, to export the Brand Module's definition, create the file `src/modules/brand/index.ts` with the following content: -To link to a page in your admin customizations, you can use the `Link` component from `react-router-dom`. For example: +![Directory structure in module after adding the definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869045/Medusa%20Book/brand-dir-overview-4_nf8ymw.jpg) -```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" +```ts title="src/modules/brand/index.ts" +import { Module } from "@medusajs/framework/utils" +import BrandModuleService from "./service" -// The widget -const ProductWidget = () => { - return ( - - View Orders - - ) -} +export const BRAND_MODULE = "brand" -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", +export default Module(BRAND_MODULE, { + service: BrandModuleService, }) - -export default ProductWidget ``` -This adds a widget to a product's details page with a link to the Orders page. The link's path must be without the `/app` prefix. - -*** - -## Admin Route Loader - -Route loaders are available starting from Medusa v2.5.1. - -In your UI route or any other custom admin route, you may need to retrieve data to use it in your route component. For example, you may want to fetch a list of products to display on a custom page. - -To do that, you can export a `loader` function in the route file, which is a [React Router loader](https://reactrouter.com/6.29.0/route/loader#loader). In this function, you can fetch and return data asynchronously. Then, in your route component, you can use the [useLoaderData](https://reactrouter.com/6.29.0/hooks/use-loader-data#useloaderdata) hook from React Router to access the data. +You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: -For example, consider the following UI route created at `src/admin/routes/custom/page.tsx`: +1. The module's name (`brand`). You'll use this name when you use this module in other customizations. +2. An object with a required property `service` indicating the module's main service. -```tsx title="src/admin/routes/custom/page.tsx" highlights={loaderHighlights} -import { Container, Heading } from "@medusajs/ui" -import { - useLoaderData, -} from "react-router-dom" +You export `BRAND_MODULE` to reference the module's name more reliably in other customizations. -export async function loader() { - // TODO fetch products +*** - return { - products: [], - } -} +## 5. Add Module to Medusa's Configurations -const CustomPage = () => { - const { products } = useLoaderData() as Awaited> +To start using your module, you must add it to Medusa's configurations in `medusa-config.ts`. - return ( -
- -
- Products count: {products.length} -
-
-
- ) -} +The object passed to `defineConfig` in `medusa-config.ts` accepts a `modules` property, whose value is an array of modules to add to the application. So, add the following in `medusa-config.ts`: -export default CustomPage +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/brand", + }, + ], +}) ``` -In this example, you first export a `loader` function that can be used to fetch data, such as products. The function returns an object with a `products` property. +The Brand Module is now added to your Medusa application. You'll start using it in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). -Then, in the `CustomPage` route component, you use the `useLoaderData` hook from React Router to access the data returned by the `loader` function. You can then use the data in your component. +*** -### Route Parameters +## 6. Generate and Run Migrations -You can also access route params in the loader function. For example, consider the following UI route created at `src/admin/routes/custom/[id]/page.tsx`: +A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations ensure that your module is re-usable and removes friction when working in a team, making it easy to reflect changes across team members' databases. -```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={loaderParamHighlights} -import { Container, Heading } from "@medusajs/ui" -import { - useLoaderData, - LoaderFunctionArgs, -} from "react-router-dom" +Learn more about migrations in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#5-generate-migrations/index.html.md). -export async function loader({ params }: LoaderFunctionArgs) { - const { id } = params - // TODO fetch product by id +[Medusa's CLI tool](https://docs.medusajs.com/resources/medusa-cli/index.html.md) allows you to generate migration files for your module, then run those migrations to reflect the changes in the database. So, run the following commands in your Medusa application's directory: - return { - id, - } -} +```bash +npx medusa db:generate brand +npx medusa db:migrate +``` -const CustomPage = () => { - const { id } = useLoaderData() as Awaited> +The `db:generate` command accepts as an argument the name of the module to generate the migrations for, and the `db:migrate` command runs all migrations that haven't been run yet in the Medusa application. - return ( -
- -
- Product ID: {id} -
-
-
- ) -} +*** -export default CustomPage -``` +## Next Step: Create Brand Workflow -Because the UI route has a route parameter `[id]`, you can access the `id` parameter in the `loader` function. The loader function accepts as a parameter an object of type `LoaderFunctionArgs` from React Router. This object has a `params` property that contains the route parameters. +The Brand Module now creates a `brand` table in the database and provides a class to manage its records. -In the loader, you can fetch data asynchronously using the route parameter and return it. Then, in the route component, you can access the data using the `useLoaderData` hook. +In the next chapter, you'll implement the functionality to create a brand in a workflow. You'll then use that workflow in a later chapter to expose an endpoint that allows admin users to create a brand. -### When to Use Route Loaders -A route loader is executed before the route is loaded. So, it will block navigation until the loader function is resolved. +# Create Brands UI Route in Admin -Only use route loaders when the route component needs data essential before rendering. Otherwise, use the JS SDK with Tanstack (React) Query as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md). This way, you can fetch data asynchronously and update the UI when the data is available. You can also use a loader to prepare some initial data that's used in the route component before the data is retrieved. +In this chapter, you'll add a UI route to the admin dashboard that shows all [brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) in a new page. You'll retrieve the brands from the server and display them in a table with pagination. -*** +### Prerequisites -## Other React Router Utilities +- [Brands Module](https://docs.medusajs.com/learn/customization/custom-features/modules/index.html.md) -### Route Handles +## 1. Get Brands API Route -Route handles are available starting from Medusa v2.5.1. +In a [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/query-linked-records/index.html.md), you learned how to add an API route that retrieves brands and their products using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll expand that API route to support pagination, so that on the admin dashboard you can show the brands in a paginated table. -In your UI route or any route file, you can export a `handle` object to define [route handles](https://reactrouter.com/start/framework/route-module#handle). The object is passed to the loader and route contexts. +Replace or create the `GET` API route at `src/api/admin/brands/route.ts` with the following: -For example: +```ts title="src/api/admin/brands/route.ts" highlights={apiRouteHighlights} +// other imports... +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -```tsx title="src/admin/routes/custom/page.tsx" -export const handle = { - sandbox: true, +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve("query") + + const { + data: brands, + metadata: { count, take, skip } = {}, + } = await query.graph({ + entity: "brand", + ...req.queryConfig, + }) + + res.json({ + brands, + count, + limit: take, + offset: skip, + }) } ``` -### React Router Components and Hooks - -Refer to [react-router-dom’s documentation](https://reactrouter.com/en/6.29.0) for components and hooks that you can use in your admin customizations. +In the API route, you use Query's `graph` method to retrieve the brands. In the method's object parameter, you spread the `queryConfig` property of the request object. This property holds configurations for pagination and retrieved fields. +The query configurations are combined from default configurations, which you'll add next, and the request's query parameters: -# Admin UI Routes +- `fields`: The fields to retrieve in the brands. +- `limit`: The maximum number of items to retrieve. +- `offset`: The number of items to skip before retrieving the returned items. -In this chapter, you’ll learn how to create a UI route in the admin dashboard. +When you pass pagination configurations to the `graph` method, the returned object has the pagination's details in a `metadata` property, whose value is an object having the following properties: -## What is a UI Route? +- `count`: The total count of items. +- `take`: The maximum number of items returned in the `data` array. +- `skip`: The number of items skipped before retrieving the returned items. -The Medusa Admin dashboard is customizable, allowing you to add new pages, called UI routes. You create a UI route as a React component showing custom content that allow admin users to perform custom actions. +You return in the response the retrieved brands and the pagination configurations. -For example, you can add a new page to show and manage product reviews, which aren't available natively in Medusa. +Learn more about pagination with Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-pagination/index.html.md). *** -## How to Create a UI Route? - -### Prerequisites - -- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) +## 2. Add Default Query Configurations -You create a UI route in a `page.tsx` file under a sub-directory of `src/admin/routes` directory. The file's path relative to `src/admin/routes` determines its path in the dashboard. The file’s default export must be the UI route’s React component. +Next, you'll set the default query configurations of the above API route and allow passing query parameters to change the configurations. -For example, create the file `src/admin/routes/custom/page.tsx` with the following content: +Medusa provides a `validateAndTransformQuery` middleware that validates the accepted query parameters for a request and sets the default Query configuration. So, in `src/api/middlewares.ts`, add a new middleware configuration object: -![Example of UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867243/Medusa%20Book/ui-route-dir-overview_tgju25.jpg) +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformQuery, +} from "@medusajs/framework/http" +import { createFindParams } from "@medusajs/medusa/api/utils/validators" +// other imports... -```tsx title="src/admin/routes/custom/page.tsx" -import { Container, Heading } from "@medusajs/ui" +export const GetBrandsSchema = createFindParams() -const CustomPage = () => { - return ( - -
- This is my custom route -
-
- ) -} +export default defineMiddlewares({ + routes: [ + // ... + { + matcher: "/admin/brands", + method: "GET", + middlewares: [ + validateAndTransformQuery( + GetBrandsSchema, + { + defaults: [ + "id", + "name", + "products.*", + ], + isList: true, + } + ), + ], + }, -export default CustomPage + ], +}) ``` -You add a new route at `http://localhost:9000/app/custom`. The `CustomPage` component holds the page's content, which currently only shows a heading. +You apply the `validateAndTransformQuery` middleware on the `GET /admin/brands` API route. The middleware accepts two parameters: -In the route, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md), a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it. +- A [Zod](https://zod.dev/) schema that a request's query parameters must satisfy. Medusa provides `createFindParams` that generates a Zod schema with the following properties: + - `fields`: A comma-separated string indicating the fields to retrieve. + - `limit`: The maximum number of items to retrieve. + - `offset`: The number of items to skip before retrieving the returned items. + - `order`: The name of the field to sort the items by. Learn more about sorting in [the API reference](https://docs.medusajs.com/api/admin#sort-order) +- An object of Query configurations having the following properties: + - `defaults`: An array of default fields and relations to retrieve. + - `isList`: Whether the API route returns a list of items. -The UI route component must be created as an arrow function. +By applying the above middleware, you can pass pagination configurations to `GET /admin/brands`, which will return a paginated list of brands. You'll see how it works when you create the UI route. -### Test the UI Route +Learn more about using the `validateAndTransformQuery` middleware to configure Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query#request-query-configurations/index.html.md). -To test the UI route, start the Medusa application: +*** -```bash npm2yarn -npm run dev -``` - -Then, after logging into the admin dashboard, open the page `http://localhost:9000/app/custom` to see your custom page. - -*** +## 3. Initialize JS SDK -## Show UI Route in the Sidebar +In your custom UI route, you'll retrieve the brands by sending a request to the Medusa server. Medusa has a [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) that simplifies sending requests to the core API route. -To add a sidebar item for your custom UI route, export a configuration object in the UI route's file: +If you didn't follow the [previous chapter](https://docs.medusajs.com/learn/customization/customize-admin/widget/index.html.md), create the file `src/admin/lib/sdk.ts` with the following content: -```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" +![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg) -const CustomPage = () => { - return ( - -
- This is my custom route -
-
- ) -} +```ts title="src/admin/lib/sdk.ts" +import Medusa from "@medusajs/js-sdk" -export const config = defineRouteConfig({ - label: "Custom Route", - icon: ChatBubbleLeftRight, +export const sdk = new Medusa({ + baseUrl: import.meta.env.VITE_BACKEND_URL || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, }) - -export default CustomPage ``` -The configuration object is created using `defineRouteConfig` from the Medusa Framework. 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](https://docs.medusajs.com/ui/icons/overview/index.html.md). - -### Nested UI Routes - -Consider that along the UI route above at `src/admin/routes/custom/page.tsx` you create a nested UI route at `src/admin/routes/custom/nested/page.tsx` that also exports route configurations: - -![Example of nested UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867243/Medusa%20Book/ui-route-dir-overview_tgju25.jpg) +You initialize the SDK passing it the following options: -```tsx title="src/admin/routes/custom/nested/page.tsx" -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" +- `baseUrl`: The URL to the Medusa server. +- `debug`: Whether to enable logging debug messages. This should only be enabled in development. +- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard. -const NestedCustomPage = () => { - return ( - -
- This is my nested custom route -
-
- ) -} +Notice that you use `import.meta.env` to access environment variables in your customizations because the Medusa Admin is built on top of Vite. Learn more in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). -export const config = defineRouteConfig({ - label: "Nested Route", -}) +You can now use the SDK to send requests to the Medusa server. -export default NestedCustomPage -``` +Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). -This UI route is shown in the sidebar as an item nested in the parent "Custom Route" item. Nested items are only shown when the parent sidebar items (in this case, "Custom Route") are clicked. +*** -#### Caveats +## 4. Add a UI Route to Show Brands -Some caveats for nested UI routes in the sidebar: +You'll now add the UI route that shows the paginated list of brands. A UI route is a React component created in a `page.tsx` file under a sub-directory of `src/admin/routes`. The file's path relative to src/admin/routes determines its path in the dashboard. -- Nested dynamic UI routes, such as one created at `src/admin/routes/custom/[id]/page.tsx` aren't added to the sidebar as it's not possible to link to a dynamic route. If the dynamic route exports route configurations, a warning is logged in the browser's console. -- Nested routes in setting pages aren't shown in the sidebar to follow the admin's design conventions. -- The `icon` configuration is ignored for the sidebar item of nested UI route to follow the admin's design conventions. +Learn more about UI routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). -### Route Under Existing Admin Route +So, to add the UI route at the `localhost:9000/app/brands` path, create the file `src/admin/routes/brands/page.tsx` with the following content: -You can add a custom UI route under an existing route. For example, you can add a route under the orders route: +![Directory structure of the Medusa application after adding the UI route.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733472011/Medusa%20Book/brands-admin-dir-overview-3_syytld.jpg) -```tsx title="src/admin/routes/orders/nested/page.tsx" +```tsx title="src/admin/routes/brands/page.tsx" highlights={uiRouteHighlights} import { defineRouteConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" +import { TagSolid } from "@medusajs/icons" +import { + Container, +} from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { sdk } from "../../lib/sdk" +import { useMemo, useState } from "react" + +const BrandsPage = () => { + // TODO retrieve brands -const NestedOrdersPage = () => { return ( -
- Nested Orders Page -
+ {/* TODO show brands */}
) } export const config = defineRouteConfig({ - label: "Nested Orders", - nested: "/orders", + label: "Brands", + icon: TagSolid, }) -export default NestedOrdersPage +export default BrandsPage ``` -The `nested` property passed to `defineRouteConfig` specifies which route this custom route is nested under. This route will now show in the sidebar under the existing "Orders" sidebar item. - -*** - -## Create Settings Page +A route's file must export the React component that will be rendered in the new page. It must be the default export of the file. You can also export configurations that add a link in the sidebar for the UI route. You create these configurations using `defineRouteConfig` from the Admin Extension SDK. -To create a page under the settings section of the admin dashboard, create a UI route under the path `src/admin/routes/settings`. +So far, you only show a container. In admin customizations, use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md) to maintain a consistent user interface and design in the dashboard. -For example, create a UI route at `src/admin/routes/settings/custom/page.tsx`: +### Retrieve Brands From API Route -![Example of settings UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867435/Medusa%20Book/setting-ui-route-dir-overview_kytbh8.jpg) +You'll now update the UI route to retrieve the brands from the API route you added earlier. -```tsx title="src/admin/routes/settings/custom/page.tsx" -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" +First, add the following type in `src/admin/routes/brands/page.tsx`: -const CustomSettingPage = () => { - return ( - -
- Custom Setting Page -
-
- ) +```tsx title="src/admin/routes/brands/page.tsx" +type Brand = { + id: string + name: string +} +type BrandsResponse = { + brands: Brand[] + count: number + limit: number + offset: number } - -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`. - -*** +You define the type for a brand, and the type of expected response from the `GET /admin/brands` API route. -## Path Parameters +To display the brands, you'll use Medusa UI's [DataTable](https://docs.medusajs.com/ui/components/data-table/index.html.md) component. So, add the following imports in `src/admin/routes/brands/page.tsx`: -A UI route can accept path parameters if the name of any of the directories in its path is of the format `[param]`. +```tsx title="src/admin/routes/brands/page.tsx" +import { + // ... + Heading, + createDataTableColumnHelper, + DataTable, + DataTablePaginationState, + useDataTable, +} from "@medusajs/ui" +``` -For example, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content: +You import the `DataTable` component and the following utilities: -![Example of UI route file with path parameters in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867748/Medusa%20Book/path-param-ui-route-dir-overview_kcfbev.jpg) +- `createDataTableColumnHelper`: A utility to create columns for the data table. +- `DataTablePaginationState`: A type that holds the pagination state of the data table. +- `useDataTable`: A hook to initialize and configure the data table. -```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, Heading } from "@medusajs/ui" +You also import the `Heading` component to show a heading above the data table. -const CustomPage = () => { - const { id } = useParams() +Next, you'll define the table's columns. Add the following before the `BrandsPage` component: - return ( - -
- Passed ID: {id} -
-
- ) -} +```tsx title="src/admin/routes/brands/page.tsx" +const columnHelper = createDataTableColumnHelper() -export default CustomPage +const columns = [ + columnHelper.accessor("id", { + header: "ID", + }), + columnHelper.accessor("name", { + header: "Name", + }), +] ``` -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](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. +You use the `createDataTableColumnHelper` utility to create columns for the data table. You define two columns for the ID and name of the brands. -*** +Then, replace the `// TODO retrieve brands` in the component with the following: -## More Routes Customizations +```tsx title="src/admin/routes/brands/page.tsx" highlights={queryHighlights} +const limit = 15 +const [pagination, setPagination] = useState({ + pageSize: limit, + pageIndex: 0, +}) +const offset = useMemo(() => { + return pagination.pageIndex * limit +}, [pagination]) -For more customizations related to routes, refer to the [Routing Customizations chapter](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md). +const { data, isLoading } = useQuery({ + queryFn: () => sdk.client.fetch(`/admin/brands`, { + query: { + limit, + offset, + }, + }), + queryKey: [["brands", limit, offset]], +}) +// TODO configure data table +``` -# Admin Widgets +To enable pagination in the `DataTable` component, you need to define a state variable of type `DataTablePaginationState`. It's an object having the following properties: -In this chapter, you’ll learn more about widgets and how to use them. +- `pageSize`: The maximum number of items per page. You set it to `15`. +- `pageIndex`: A zero-based index of the current page of items. -## What is an Admin Widget? +You also define a memoized `offset` value that indicates the number of items to skip before retrieving the current page's items. -The Medusa Admin dashboard's pages are customizable to insert widgets of custom content in pre-defined injection zones. You create these widgets as React components that allow admin users to perform custom actions. +Then, you use `useQuery` from [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. -For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. +Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. -*** +In the `queryFn` function that executes the query, you use the JS SDK's `client.fetch` method to send a request to your custom API route. The first parameter is the route's path, and the second is an object of request configuration and data. You pass the query parameters in the `query` property. -## How to Create a Widget? +This sends a request to the [Get Brands API route](#1-get-brands-api-route), passing the pagination query parameters. Whenever `currentPage` is updated, the `offset` is also updated, which will send a new request to retrieve the brands for the current page. -### Prerequisites +### Display Brands Table -- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) +Finally, you'll display the brands in a data table. Replace the `// TODO configure data table` in the component with the following: -You create a widget in a `.tsx` file under the `src/admin/widgets` directory. The file’s default export must be the widget, which is the React component that renders the custom content. The file must also export the widget’s configurations indicating where to insert the widget. - -For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: - -![Example of widget file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867137/Medusa%20Book/widget-dir-overview_dqsbct.jpg) +```tsx title="src/admin/routes/brands/page.tsx" +const table = useDataTable({ + columns, + data: data?.brands || [], + getRowId: (row) => row.id, + rowCount: data?.count || 0, + isLoading, + pagination: { + state: pagination, + onPaginationChange: setPagination, + }, +}) +``` -```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" +You use the `useDataTable` hook to initialize and configure the data table. It accepts an object with the following properties: -// The widget -const ProductWidget = () => { - return ( - -
- Product Widget -
-
- ) -} +- `columns`: The columns of the data table. You created them using the `createDataTableColumnHelper` utility. +- `data`: The brands to display in the table. +- `getRowId`: A function that returns a unique identifier for a row. +- `rowCount`: The total count of items. This is used to determine the number of pages. +- `isLoading`: A boolean indicating whether the data is loading. +- `pagination`: An object to configure pagination. It accepts the following properties: + - `state`: The pagination state of the data table. + - `onPaginationChange`: A function to update the pagination state. -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) +Then, replace the `{/* TODO show brands */}` in the return statement with the following: -export default ProductWidget +```tsx title="src/admin/routes/brands/page.tsx" + + + Brands + + + + ``` -You export the `ProductWidget` component, which shows the heading `Product Widget`. In the widget, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md), a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it. - -To export the widget's configurations, you use `defineWidgetConfig` from the Admin Extension SDK. It accepts as a parameter an object with the `zone` property, whose value is 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. +This renders the data table that shows the brands with pagination. The `DataTable` component accepts the `instance` prop, which is the object returned by the `useDataTable` hook. -The widget component must be created as an arrow function. +*** -### Test the Widget +## Test it Out -To test out the widget, start the Medusa application: +To test out the UI route, 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. +Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, you'll find a new "Brands" sidebar item. Click on it to see the brands in your store. You can also go to `http://localhost:9000/app/brands` to see the page. + +![A new sidebar item is added for the new brands UI route. The UI route shows the table of brands with pagination.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733421074/Medusa%20Book/Screenshot_2024-12-05_at_7.46.52_PM_slcdqd.png) *** -## Props Passed in Detail Pages +## Summary -Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. +By following the previous chapters, you: -For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: +- Injected a widget into the product details page to show the product's brand. +- Created a UI route in the Medusa Admin that shows the list of brands. -```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) => { - return ( - -
- - Product Widget {data.title} - -
-
- ) -} +## Next Steps: Integrate Third-Party Systems -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) +Your customizations often span across systems, where you need to retrieve data or perform operations in a third-party system. -export default ProductWidget -``` +In the next chapters, you'll learn about the concepts that facilitate integrating third-party systems in your application. You'll integrate a dummy third-party system and sync the brands between it and the Medusa application. -The props type is `DetailWidgetProps`, and it accepts as a type argument the expected type of `data`. For the product details page, it's `AdminProduct`. -*** +# Guide: Create Brand Workflow -## Injection Zone +This chapter builds on the work from the [previous chapter](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) where you created a Brand Module. -Refer to [this reference](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md) for the full list of injection zones and their props. +After adding custom modules to your application, you build commerce features around them using workflows. A workflow is a series of queries and actions, called steps, that complete a task spanning across modules. You construct a workflow similar to a regular function, but it's a special function that allows you to define roll-back logic, retry configurations, and more advanced features. -*** +The workflow you'll create in this chapter will use the Brand Module's service to implement the feature of creating a brand. In the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll expose an API route that allows admin users to create a brand, and you'll use this workflow in the route's implementation. -## Admin Components List +Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). -To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. +### Prerequisites +- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) -# Handling CORS in API Routes +*** -In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes. +## 1. Create createBrandStep -## CORS Overview +A workflow consists of a series of steps, each step created in a TypeScript or JavaScript file under the `src/workflows` directory. A step is defined using `createStep` from the Workflows SDK -Cross-Origin Resource Sharing (CORS) allows only configured origins to access your API Routes. +The workflow you're creating in this guide has one step to create the brand. So, create the file `src/workflows/create-brand.ts` with the following content: -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. +![Directory structure in the Medusa project after adding the file for createBrandStep](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869184/Medusa%20Book/brand-workflow-dir-overview-1_fjvf5j.jpg) -### CORS Configurations +```ts title="src/workflows/create-brand.ts" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { BRAND_MODULE } from "../modules/brand" +import BrandModuleService from "../modules/brand/service" -The `storeCors` and `adminCors` properties of Medusa's `http` configuration set the allowed origins for routes starting with `/store` and `/admin` respectively. +export type CreateBrandStepInput = { + name: string +} -These configurations accept a URL pattern to identify allowed origins. +export const createBrandStep = createStep( + "create-brand-step", + async (input: CreateBrandStepInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) -For example: + const brand = await brandModuleService.createBrands(input) -```js title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - http: { - storeCors: "http://localhost:8000", - adminCors: "http://localhost:7001", - // ... - }, - }, -}) + return new StepResponse(brand, brand.id) + } +) ``` -This allows the `http://localhost:7001` origin to access the Admin API Routes, and the `http://localhost:8000` origin to access Store API Routes. +You create a `createBrandStep` using the `createStep` function. It accepts the step's unique name as a first parameter, and the step's function as a second parameter. -Learn more about the CORS configurations in [this resource guide](https://docs.medusajs.com/learn/configurations/medusa-config#http/index.html.md). +The step function receives two parameters: input passed to the step when it's invoked, and an object of general context and configurations. This object has a `container` property, which is the Medusa container. -*** +The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) is a registry of framework and commerce tools accessible in your customizations, such as a workflow's step. The Medusa application registers the services of core and custom modules in the container, allowing you to resolve and use them. -## CORS in Store and Admin Routes +So, In the step function, you use the Medusa container to resolve the Brand Module's service and use its generated `createBrands` method, which accepts an object of brands to create. -To disable the CORS middleware for a route, export a `CORS` variable in the route file with its value set to `false`. +Learn more about the generated `create` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/create/index.html.md). -For example: +A step must return an instance of `StepResponse`. Its first parameter is the data returned by the step, and the second is the data passed to the compensation function, which you'll learn about next. -```ts title="src/api/store/custom/route.ts" highlights={[["15"]]} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +### Add Compensation Function to Step -export const GET = ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "[GET] Hello world!", - }) -} +You define for each step a compensation function that's executed when an error occurs in the workflow. The compensation function defines the logic to roll-back the changes made by the step. This ensures your data remains consistent if an error occurs, which is especially useful when you integrate third-party services. -export const CORS = false -``` +Learn more about the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). -This disables the CORS middleware on API Routes at the path `/store/custom`. +To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`: -*** +```ts title="src/workflows/create-brand.ts" +export const createBrandStep = createStep( + // ... + async (id: string, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) -## CORS in Custom Routes + await brandModuleService.deleteBrands(id) + } +) +``` -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. +The compensation function's first parameter is the brand's ID which you passed as a second parameter to the step function's returned `StepResponse`. It also accepts a context object with a `container` property as a second parameter, similar to the step function. -You can do that in the exported middlewares configurations in `src/api/middlewares.ts`. +In the compensation function, you resolve the Brand Module's service from the Medusa container, then use its generated `deleteBrands` method to delete the brand created by the step. This method accepts the ID of the brand to delete. -For example: +Learn more about the generated `delete` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/delete/index.html.md). -```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-10" expandButtonLabel="Show Imports" -import { defineMiddlewares } from "@medusajs/framework/http" -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" +So, if an error occurs during the workflow's execution, the brand that was created by the step is deleted to maintain data consistency. -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) - }, - ], - }, - ], -}) -``` +## 2. Create createBrandWorkflow -This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`. +You can now create the workflow that runs the `createBrandStep`. A workflow is created in a TypeScript or JavaScript file under the `src/workflows` directory. In the file, you use `createWorkflow` from the Workflows SDK to create the workflow. +Add the following content in the same `src/workflows/create-brand.ts` file: -# Pass Additional Data to Medusa's API Route - -In this chapter, you'll learn how to pass additional data in requests to Medusa's API Route. +```ts title="src/workflows/create-brand.ts" +// other imports... +import { + // ... + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -## 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. +type CreateBrandWorkflowInput = { + name: string +} -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. +export const createBrandWorkflow = createWorkflow( + "create-brand", + (input: CreateBrandWorkflowInput) => { + const brand = createBrandStep(input) -For example, the [Create Product API Route](https://docs.medusajs.com/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. + return new WorkflowResponse(brand) + } +) +``` -### API Routes Accepting Additional Data +You create the `createBrandWorkflow` using the `createWorkflow` function. This function accepts two parameters: the workflow's unique name, and the workflow's constructor function holding the workflow's implementation. -### API Routes List +The constructor function accepts the workflow's input as a parameter. In the function, you invoke the `createBrandStep` you created in the previous step to create a brand. -- Campaigns - - [Create Campaign](https://docs.medusajs.com/api/admin#campaigns_postcampaigns) - - [Update Campaign](https://docs.medusajs.com/api/admin#campaigns_postcampaignsid) -- Cart - - [Create Cart](https://docs.medusajs.com/api/store#carts_postcarts) - - [Update Cart](https://docs.medusajs.com/api/store#carts_postcartsid) -- Collections - - [Create Collection](https://docs.medusajs.com/api/admin#collections_postcollections) - - [Update Collection](https://docs.medusajs.com/api/admin#collections_postcollectionsid) -- Customers - - [Create Customer](https://docs.medusajs.com/api/admin#customers_postcustomers) - - [Update Customer](https://docs.medusajs.com/api/admin#customers_postcustomersid) - - [Create Address](https://docs.medusajs.com/api/admin#customers_postcustomersidaddresses) - - [Update Address](https://docs.medusajs.com/api/admin#customers_postcustomersidaddressesaddress_id) -- Draft Orders - - [Create Draft Order](https://docs.medusajs.com/api/admin#draft-orders_postdraftorders) -- Orders - - [Complete Orders](https://docs.medusajs.com/api/admin#orders_postordersidcomplete) - - [Cancel Order's Fulfillment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idcancel) - - [Create Shipment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idshipments) - - [Create Fulfillment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillments) -- Products - - [Create Product](https://docs.medusajs.com/api/admin#products_postproducts) - - [Update Product](https://docs.medusajs.com/api/admin#products_postproductsid) - - [Create Product Variant](https://docs.medusajs.com/api/admin#products_postproductsidvariants) - - [Update Product Variant](https://docs.medusajs.com/api/admin#products_postproductsidvariantsvariant_id) - - [Create Product Option](https://docs.medusajs.com/api/admin#products_postproductsidoptions) - - [Update Product Option](https://docs.medusajs.com/api/admin#products_postproductsidoptionsoption_id) -- Product Tags - - [Create Product Tag](https://docs.medusajs.com/api/admin#product-tags_postproducttags) - - [Update Product Tag](https://docs.medusajs.com/api/admin#product-tags_postproducttagsid) -- Product Types - - [Create Product Type](https://docs.medusajs.com/api/admin#product-types_postproducttypes) - - [Update Product Type](https://docs.medusajs.com/api/admin#product-types_postproducttypesid) -- Promotions - - [Create Promotion](https://docs.medusajs.com/api/admin#promotions_postpromotions) - - [Update Promotion](https://docs.medusajs.com/api/admin#promotions_postpromotionsid) +A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor. *** -## How to Pass Additional Data +## Next Steps: Expose Create Brand API Route -### 1. Specify Validation of Additional Data +You now have a `createBrandWorkflow` that you can execute to create a brand. -Before passing custom data in the `additional_data` object parameter, you must specify validation rules for the allowed properties in the object. +In the next chapter, you'll add an API route that allows admin users to create a brand. You'll learn how to create the API route, and execute in it the workflow you implemented in this chapter. -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: +# Guide: Add Product's Brand Widget in Admin -```ts title="src/api/middlewares.ts" -import { defineMiddlewares } from "@medusajs/framework/http" -import { z } from "zod" +In this chapter, you'll customize the product details page of the Medusa Admin dashboard to show the product's [brand](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md). You'll create a widget that is injected into a pre-defined zone in the page, and in the widget you'll retrieve the product's brand from the server and display it. -export default defineMiddlewares({ - routes: [ - { - method: "POST", - matcher: "/admin/products", - additionalDataValidator: { - brand: z.string().optional(), - }, - }, - ], -}) -``` +### Prerequisites -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. +- [Brands linked to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) -In this example, you indicate that the `additional_data` parameter accepts a `brand` property whose value is an optional string. +## 1. Initialize JS SDK -Refer to [Zod's documentation](https://zod.dev) for all available validation rules. +In your custom widget, you'll retrieve the product's brand by sending a request to the Medusa server. Medusa has a [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) that simplifies sending requests to the server's API routes. -### 2. Pass the Additional Data in a Request +So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content: -You can now pass a `brand` property in the `additional_data` parameter of a request to the Create Product API Route. +![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg) -For example: +```ts title="src/admin/lib/sdk.ts" +import Medusa from "@medusajs/js-sdk" -```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"] - } - ], - "shipping_profile_id": "{shipping_profile_id}", - "additional_data": { - "brand": "Acme" - } -}' +export const sdk = new Medusa({ + baseUrl: import.meta.env.VITE_BACKEND_URL || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, +}) ``` -Make sure to replace the `{token}` in the authorization header with an admin user's authentication token, and `{shipping_profile_id}` with an existing shipping profile's ID. +You initialize the SDK passing it the following options: -In this request, you pass in the `additional_data` parameter a `brand` property and set its value to `Acme`. +- `baseUrl`: The URL to the Medusa server. +- `debug`: Whether to enable logging debug messages. This should only be enabled in development. +- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard. -The `additional_data` is then passed to hooks in the `createProductsWorkflow` used by the API route. +Notice that you use `import.meta.env` to access environment variables in your customizations because the Medusa Admin is built on top of Vite. Learn more in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). + +You can now use the SDK to send requests to the Medusa server. + +Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). *** -## Use Additional Data in a Hook +## 2. Add Widget to Product Details Page -Learn about workflow hooks in [this guide](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). +You'll now add a widget to the product-details page. A widget is a React component that's injected into pre-defined zones in the Medusa Admin dashboard. It's created in a `.tsx` file under the `src/admin/widgets` directory. -Step functions consuming the workflow hook can access the `additional_data` in the first parameter. +Learn more about widgets in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md). -For example, consider you want to store the data passed in `additional_data` in the product's `metadata` property. +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: -To do that, create the file `src/workflows/hooks/product-created.ts` with the following content: +![Directory structure of the Medusa application after adding the widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414684/Medusa%20Book/brands-admin-dir-overview-2_eq5xhi.jpg) -```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" +```tsx title="src/admin/widgets/product-brand.tsx" highlights={highlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { DetailWidgetProps, AdminProduct } from "@medusajs/framework/types" +import { clx, Container, Heading, Text } from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { sdk } from "../lib/sdk" -createProductsWorkflow.hooks.productsCreated( - async ({ products, additional_data }, { container }) => { - if (!additional_data?.brand) { - return - } +type AdminProductBrand = AdminProduct & { + brand?: { + id: string + name: string + } +} - const productModuleService = container.resolve( - Modules.PRODUCT - ) +const ProductBrandWidget = ({ + data: product, +}: DetailWidgetProps) => { + const { data: queryResult } = useQuery({ + queryFn: () => sdk.admin.product.retrieve(product.id, { + fields: "+brand.*", + }), + queryKey: [["product", product.id]], + }) + const brandName = (queryResult?.product as AdminProductBrand)?.brand?.name - await productModuleService.upsertProducts( - products.map((product) => ({ - ...product, - metadata: { - ...product.metadata, - brand: additional_data.brand, - }, - })) - ) + return ( + +
+
+ Brand +
+
+
+ + Name + - return new StepResponse(products, { - products, - additional_data, - }) - } -) + + {brandName || "-"} + +
+
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductBrandWidget ``` -This consumes the `productsCreated` hook, which runs after the products are created. +A widget's file must export: -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. +- A React component to be rendered in the specified injection zone. The component must be the file's default export. +- A configuration object created with `defineWidgetConfig` from the Admin Extension SDK. The function receives an object as a parameter that has a `zone` property, whose value is the zone to inject the widget to. -### Compensation Function +Since the widget is injected at the top of the product details page, the widget receives the product's details as a parameter. -Hooks also accept a compensation function as a second parameter to undo the actions made by the step function. +In the widget, you use [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. In the `queryFn` function that executes the query, you use the JS SDK to send a request to the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid), passing `+brand.*` in the `fields` query parameter to retrieve the product's brand. -For example, pass the following second parameter to the `productsCreated` hook: +Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. -```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 - } +You then render a section that shows the brand's name. In admin customizations, use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md) to maintain a consistent user interface and design in the dashboard. - const productModuleService = container.resolve( - Modules.PRODUCT - ) +*** - await productModuleService.upsertProducts( - products - ) - } -) +## Test it Out + +To test out your widget, start the Medusa application: + +```bash npm2yarn +npm run dev ``` -This updates the products to their original state before adding the brand to their `metadata` property. +Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, open the page of a product that has a brand. You'll see a new section at the top showing the brand's name. +![The widget is added as the first section of the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414415/Medusa%20Book/Screenshot_2024-12-05_at_5.59.25_PM_y85m14.png) -# HTTP Methods +*** -In this chapter, you'll learn about how to add new API routes for each HTTP method. +## Admin Components Guides -## HTTP Method Handler +When building your widget, you may need more complicated components. For example, you may add a form to the above widget to set the product's brand. -An API route is created for every HTTP method you export a handler function for in a route file. +The [Admin Components guides](https://docs.medusajs.com/resources/admin-components/index.html.md) show you how to build and use common components in the Medusa Admin, such as forms, tables, JSON data viewer, and more. The components in the guides also follow the Medusa Admin's design convention. -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: +## Next Chapter: Add UI Route for Brands -```ts title="src/api/hello-world/route.ts" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users. -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!", - }) -} -``` +# Guide: Define Module Link Between Brand and Product -This adds two API Routes: +In this chapter, you'll learn how to define a module link between a brand defined in the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), and a product defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) that's available in your Medusa application out-of-the-box. -- A `GET` route at `http://localhost:9000/hello-world`. -- A `POST` route at `http://localhost:9000/hello-world`. +Modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) from other resources, ensuring that they're integrated into the Medusa application without side effects. However, you may need to associate data models of different modules, or you're trying to extend data models from commerce modules with custom properties. To do that, you define module links. +A module link forms an association between two data models of different modules while maintaining module isolation. You can then manage and query linked records of the data models using Medusa's Modules SDK. -# Throwing and Handling Errors +In this chapter, you'll define a module link between the `Brand` data model of the Brand Module, and the `Product` data model of the Product Module. In later chapters, you'll manage and retrieve linked product and brand records. -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. +Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). -## Throw MedusaError +### Prerequisites -When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError` from the Medusa Framework. +- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) -The Medusa application's API route error handler then wraps your thrown error in a uniform object and returns it in the response. +## 1. Define Link -For example: +Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines and exports the link using `defineLink` from the Modules SDK. -```ts -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { MedusaError } from "@medusajs/framework/utils" +So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content: -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 directory structure of the Medusa application after adding the link.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733329897/Medusa%20Book/brands-link-dir-overview_t1rhlp.jpg) - // ... -} +```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 `MedusaError` class accepts in its constructor two parameters: +You import each module's definition object from the `index.ts` file of the module's directory. Each module object has a special `linkable` property that holds the data models' link configurations. -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. +The `defineLink` function accepts two parameters of the same type, which is either: -### Error Object in Response +- The data model's link configuration, which you access from the Module's `linkable` property; +- Or an object that has two properties: + - `linkable`: the data model's link configuration, which you access from the Module's `linkable` property. + - `isList`: A boolean indicating whether many records of the data model can be linked to the other model. -The error object returned in the response has two properties: +So, in the above code snippet, you define a link between the `Product` and `Brand` data models. Since a brand can be associated with multiple products, you enable `isList` in the `Product` model's object. -- `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 +## 2. Sync the Link to the Database -|Type|Description|Status Code| -|---|---|---|---|---| -|\`DB\_ERROR\`|Indicates a database error.|\`500\`| -|\`DUPLICATE\_ERROR\`|Indicates a duplicate of a record already exists. For example, when trying to create a customer whose email is registered by another customer.|\`422\`| -|\`INVALID\_ARGUMENT\`|Indicates an error that occurred due to incorrect arguments or other unexpected state.|\`500\`| -|\`INVALID\_DATA\`|Indicates a validation error.|\`400\`| -|\`UNAUTHORIZED\`|Indicates that a user is not authorized to perform an action or access a route.|\`401\`| -|\`NOT\_FOUND\`|Indicates that the requested resource, such as a route or a record, isn't found.|\`404\`| -|\`NOT\_ALLOWED\`|Indicates that an operation isn't allowed.|\`400\`| -|\`CONFLICT\`|Indicates that a request conflicts with another previous or ongoing request. The error message in this case is ignored for a default message.|\`409\`| -|\`PAYMENT\_AUTHORIZATION\_ERROR\`|Indicates an error has occurred while authorizing a payment.|\`422\`| -|Other error types|Any other error type results in an |\`500\`| +A module link is represented in the database as a table that stores the IDs of linked records. So, after defining the link, run the following command to create the module link's table in the database: -*** +```bash +npx medusa db:migrate +``` -## Override Error Handler +This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link. -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. +You can also run the `npx medusa db:sync-links` to just sync module links without running migrations. -This error handler will also be used for errors thrown in Medusa's API routes and resources. +*** -For example, create `src/api/middlewares.ts` with the following: +## Next Steps: Extend Create Product Flow -```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" +In the next chapter, you'll extend Medusa's workflow and API route that create a product to allow associating a brand with a product. You'll also learn how to link brand and product records. -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: +# Guide: Extend Create Product Flow -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. +After linking the [custom Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) in the [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md), you'll extend the create product workflow and API route to allow associating a brand with a product. -This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message. +Some API routes, including the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), accept an `additional_data` request body parameter. This parameter can hold custom data that's passed to the [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) of the workflow executed in the API route, allowing you to consume those hooks and perform actions with the custom data. +So, in this chapter, to extend the create product flow and associate a brand with a product, you will: -# API Route Parameters +- Consume the [productsCreated](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow#productsCreated/index.html.md) hook of the [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md), which is executed within the workflow after the product is created. You'll link the product with the brand passed in the `additional_data` parameter. +- Extend the Create Product API route to allow passing a brand ID in `additional_data`. -In this chapter, you’ll learn about path, query, and request body parameters. +To learn more about the `additional_data` property and the API routes that accept additional data, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md). -## Path Parameters +### Prerequisites -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]`. +- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) -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: +*** -```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +## 1. Consume the productsCreated Hook -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[GET] Hello ${req.params.id}!`, - }) -} -``` +A workflow hook is a point in a workflow where you can inject a step to perform a custom functionality. Consuming a workflow hook allows you to extend the features of a workflow and, consequently, the API route that uses it. -The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs. +Learn more about the workflow hooks in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). -### Multiple Path Parameters +The [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) used in the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts) has a `productsCreated` hook that runs after the product is created. You'll consume this hook to link the created product with the brand specified in the request 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]`. +To consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content: -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: +![Directory structure after creating the hook's file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733384338/Medusa%20Book/brands-hook-dir-overview_ltwr5h.jpg) -```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +```ts title="src/workflows/hooks/created-product.ts" highlights={hook1Highlights} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +import { StepResponse } from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" +import { LinkDefinition } from "@medusajs/framework/types" +import { BRAND_MODULE } from "../../modules/brand" +import BrandModuleService from "../../modules/brand/service" -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[GET] Hello ${ - req.params.id - } - ${req.params.name}!`, +createProductsWorkflow.hooks.productsCreated( + (async ({ products, additional_data }, { container }) => { + if (!additional_data?.brand_id) { + return new StepResponse([], []) + } + + 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) + + // TODO link brand to product }) -} +) ``` -You access the `id` and `name` path parameters using the `req.params` property. +Workflows have a special `hooks` property to access its hooks and consume them. Each hook, such as `productsCreated`, accepts a step function as a parameter. The step function accepts the following parameters: -*** +1. An object having an `additional_data` property, which is the custom data passed in the request body under `additional_data`. The object will also have properties passed from the workflow to the hook, which in this case is the `products` property that holds an array of the created products. +2. An object of properties related to the step's context. It has a `container` property whose value is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) to resolve framework and commerce tools. -## Query Parameters +In the step, if a brand ID is passed in `additional_data`, you resolve the Brand Module's service and use its generated `retrieveBrand` method to retrieve the brand by its ID. The `retrieveBrand` method will throw an error if the brand doesn't exist. -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. +### Link Brand to Product -For example: +Next, you want to create a link between the created products and the brand. To do so, you use Link, which is a class from the Modules SDK that provides methods to manage linked records. -```ts title="src/api/hello-world/route.ts" highlights={queryHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +Learn more about Link in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md). -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `Hello ${req.query.name}`, +To use Link in the `productsCreated` hook, replace the `TODO` with the following: + +```ts title="src/workflows/hooks/created-product.ts" highlights={hook2Highlights} +const link = container.resolve("link") +const logger = container.resolve("logger") + +const links: LinkDefinition[] = [] + +for (const product of products) { + links.push({ + [Modules.PRODUCT]: { + product_id: product.id, + }, + [BRAND_MODULE]: { + brand_id: additional_data.brand_id, + }, }) } -``` -The value of `req.query.name` is the value passed in `?name=John`, for example. +await link.create(links) -### Validate Query Parameters +logger.info("Linked brand to products") -You can apply validation rules on received query parameters to ensure they match specified rules and types. +return new StepResponse(links, links) +``` -Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-query-paramters/index.html.md). +You resolve Link from the container. Then you loop over the created products to assemble an array of links to be created. After that, you pass the array of links to Link's `create` method, which will link the product and brand records. -*** +Each property in the link object is the name of a module, and its value is an object having a `{model_name}_id` property, where `{model_name}` is the snake-case name of the module's data model. Its value is the ID of the record to be linked. The link object's properties must be set in the same order as the link configurations passed to `defineLink`. -## Request Body Parameters +![Diagram showcasing how the order of defining a link affects creating the link](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386156/Medusa%20Book/remote-link-brand-product-exp_fhjmg4.jpg) -The Medusa application parses the body of any request having a JSON, URL-encoded, or text request content types. The request body parameters are set in the `MedusaRequest`'s `body` property. +Finally, you return an instance of `StepResponse` returning the created links. -Learn more about configuring body parsing in [this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md). +### Dismiss Links in Compensation -For example: +You can pass as a second parameter of the hook a compensation function that undoes what the step did. It receives as a first parameter the returned `StepResponse`'s second parameter, and the step context object as a second parameter. -```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +To undo creating the links in the hook, pass the following compensation function as a second parameter to `productsCreated`: -type HelloWorldReq = { - name: string -} +```ts title="src/workflows/hooks/created-product.ts" +createProductsWorkflow.hooks.productsCreated( + // ... + (async (links, { container }) => { + if (!links?.length) { + return + } -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[POST] Hello ${req.body.name}!`, + const link = container.resolve("link") + + await link.dismiss(links) }) -} +) ``` -In this example, you use the `name` request body parameter to create the message in the returned response. +In the compensation function, if the `links` parameter isn't empty, you resolve Link from the container and use its `dismiss` method. This method removes a link between two records. It accepts the same parameter as the `create` method. -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. +*** -To test it out, send the following request to your Medusa application: +## 2. Configure Additional Data Validation -```bash -curl -X POST 'http://localhost:9000/hello-world' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "name": "John" -}' -``` +Now that you've consumed the `productsCreated` hook, you want to configure the `/admin/products` API route that creates a new product to accept a brand ID in its `additional_data` parameter. -This returns the following JSON object: +You configure the properties accepted in `additional_data` in the `src/api/middlewares.ts` that exports middleware configurations. So, create the file (or, if already existing, add to the file) `src/api/middlewares.ts` the following content: -```json -{ - "message": "[POST] Hello John!" -} -``` +![Directory structure after adding the middelwares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386868/Medusa%20Book/brands-middleware-dir-overview_uczos1.jpg) -### Validate Body Parameters +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/framework/http" +import { z } from "zod" -You can apply validation rules on received body parameters to ensure they match specified rules and types. +// ... -Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md). +export default defineMiddlewares({ + routes: [ + // ... + { + matcher: "/admin/products", + method: ["POST"], + additionalDataValidator: { + brand_id: z.string().optional(), + }, + }, + ], +}) +``` +Objects in `routes` accept an `additionalDataValidator` property that configures the validation rules for custom properties passed in the `additional_data` request parameter. It accepts an object whose keys are custom property names, and their values are validation rules created using [Zod](https://zod.dev/). -# Middlewares +So, `POST` requests sent to `/admin/products` can now pass the ID of a brand in the `brand_id` property of `additional_data`. -In this chapter, you’ll learn about middlewares and how to create them. +*** -## What is a Middleware? +## Test it Out -A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler function. +To test it out, first, retrieve the authentication token of your admin user by sending a `POST` request to `/auth/user/emailpass`: -Middlwares are used to guard API routes, parse request content types other than `application/json`, manipulate request data, and more. +```bash +curl -X POST 'http://localhost:9000/auth/user/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "admin@medusa-test.com", + "password": "supersecret" +}' +``` -As Medusa's server is based on Express, you can use any [Express middleware](https://expressjs.com/en/resources/middleware.html). +Make sure to replace the email and password in the request body with your user's credentials. -### Middleware Types +Then, send a `POST` request to `/admin/products` to create a product, and pass in the `additional_data` parameter a brand's ID: -There are two types of middlewares: +```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"] + } + ], + "shipping_profile_id": "{shipping_profile_id}", + "additional_data": { + "brand_id": "{brand_id}" + } +}' +``` -1. Global Middleware: A middleware that applies to all routes matching a specified pattern. -2. Route Middleware: A middleware that applies to routes matching a specified pattern and HTTP method(s). +Make sure to replace `{token}` with the token you received from the previous request, `shipping_profile_id` with the ID of a shipping profile in your application, and `{brand_id}` with the ID of a brand in your application. You can retrieve the ID of a shipping profile either from the Medusa Admin, or the [List Shipping Profiles API route](https://docs.medusajs.com/api/admin#shipping-profiles_getshippingprofiles). -These middlewares generally have the same definition and usage, but they differ in the routes they apply to. You'll learn how to create both types in the following sections. +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. *** -## How to Create a Global Middleware? +## Next Steps: Query Linked Brands and Products -Middlewares of all types are defined in the special file `src/api/middlewares.ts`. Use the `defineMiddlewares` function from the Medusa Framework to define the middlewares, and export its value. +Now that you've extending the create-product flow to link a brand to it, you want to retrieve the brand details of a product. You'll learn how to do so in the next chapter. -For example: -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - MedusaNextFunction, - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +# Guide: Query Product's Brands -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom*", - middlewares: [ - ( - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - console.log("Received a request!") +In the previous chapters, you [defined a link](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) between the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md), then [extended the create-product flow](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product/index.html.md) to link a product to a brand. - next() - }, - ], - }, - ], -}) -``` +In this chapter, you'll learn how to retrieve a product's brand (and vice-versa) in two ways: Using Medusa's existing API route, or in customizations, such as a custom API route. -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: +### Prerequisites -- `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 global and route middleware functions. +- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) -In the example above, you define a global middleware that logs the message `Received a request!` whenever a request is sent to an API route path starting with `/custom`. +*** -### Test the Global Middleware +## Approach 1: Retrieve Brands in Existing API Routes -To test the middleware: +Medusa's existing API routes accept a `fields` query parameter that allows you to specify the fields and relations of a model to retrieve. So, when you send a request to the [List Products](https://docs.medusajs.com/api/admin#products_getproducts), [Get Product](https://docs.medusajs.com/api/admin#products_getproductsid), or any product-related store or admin routes that accept a `fields` query parameter, you can specify in this parameter to return the product's brands. -1. Start the application: +Learn more about selecting fields and relations in the [API Reference](https://docs.medusajs.com/api/admin#select-fields-and-relations). -```bash npm2yarn -npm run dev +For example, send the following request to retrieve the list of products with their brands: + +```bash +curl 'http://localhost:9000/admin/products?fields=+brand.*' \ +--header 'Authorization: Bearer {token}' ``` -2. Send a request to any API route starting with `/custom`. -3. See the following message in the terminal: +Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). -```bash -Received a request! +Any product that is linked to a brand will have a `brand` property in its object: + +```json title="Example Product Object" +{ + "id": "prod_123", + // ... + "brand": { + "id": "01JEB44M61BRM3ARM2RRMK7GJF", + "name": "Acme", + "created_at": "2024-12-05T09:59:08.737Z", + "updated_at": "2024-12-05T09:59:08.737Z", + "deleted_at": null + } +} ``` +By using the `fields` query parameter, you don't have to re-create existing API routes to get custom data models that you linked to core data models. + *** -## How to Create a Route Middleware? +## Approach 2: Use Query to Retrieve Linked Records -In the previous section, you learned how to create a global middleware. You define the route middleware in the same way in `src/api/middlewares.ts`, but you specify an additional property `method` in the middleware route object. Its value is one or more HTTP methods to apply the middleware to. +You can also retrieve linked records using Query. Query allows you to retrieve data across modules with filters, pagination, and more. You can resolve Query from the Medusa container and use it in your API route or workflow. -For example: +Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). -```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - MedusaNextFunction, - MedusaRequest, - MedusaResponse, - defineMiddlewares, +For example, you can create an API route that retrieves brands and their products. If you followed the [Create Brands API route chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll have the file `src/api/admin/brands/route.ts` with a `POST` API route. Add a new `GET` function to the same file: + +```ts title="src/api/admin/brands/route.ts" highlights={highlights} +// other imports... +import { + MedusaRequest, + MedusaResponse, } from "@medusajs/framework/http" -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom*", - method: ["POST", "PUT"], - middlewares: [ - ( - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - console.log("Received a request!") +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve("query") + + const { data: brands } = await query.graph({ + entity: "brand", + fields: ["*", "products.*"], + }) - next() - }, - ], - }, - ], -}) + res.json({ brands }) +} ``` -This example applies the middleware only when a `POST` or `PUT` request is sent to an API route path starting with `/custom`, changing the middleware from a global middleware to a route middleware. - -### Test the Route Middleware +This adds a `GET` API route at `/admin/brands`. In the API route, you resolve Query from the Medusa container. Query has a `graph` method that runs a query to retrieve data. It accepts an object having the following properties: -To test the middleware: +- `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`, or `*` for all properties. + - A relation or linked model's name, such as `products` (use the plural name since brands are linked to list of products). You suffix the name with `.*` to retrieve all its properties. -1. Start the application: +`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. -```bash npm2yarn -npm run dev -``` +### Test it Out -2. Send a `POST` request to any API route starting with `/custom`. -3. See the following message in the terminal: +To test the API route out, send a `GET` request to `/admin/brands`: ```bash -Received a request! +curl 'http://localhost:9000/admin/brands' \ +-H 'Authorization: Bearer {token}' ``` -*** +Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). -## When to Use Middlewares +This returns the brands in your store with their linked products. For example: -- You want to protect API routes by a custom condition. -- You're modifying the request body. +```json title="Example Response" +{ + "brands": [ + { + "id": "123", + // ... + "products": [ + { + "id": "prod_123", + // ... + } + ] + } + ] +} +``` *** -## Middleware Function Parameters - -The middleware function accepts three parameters: +## Summary -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. +By following the examples of the previous chapters, you: -You must call the `next` function in the middleware. Otherwise, other middlewares and the API route handler won’t execute. +- Defined a link between the Brand and Product modules's data models, allowing you to associate a product with a brand. +- Extended the create-product workflow and route to allow setting the product's brand while creating the product. +- Queried a product's brand, and vice versa. *** -## Middleware for Routes with Path Parameters - -To indicate a path parameter in a middleware's `matcher` pattern, use the format `:{param-name}`. +## Next Steps: Customize Medusa Admin -For example: +Clients, such as the Medusa Admin dashboard, can now use brand-related features, such as creating a brand or setting the brand of a product. -```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports" highlights={pathParamHighlights} -import { - MedusaNextFunction, - MedusaRequest, - MedusaResponse, - defineMiddlewares, -} from "@medusajs/framework/http" +In the next chapters, you'll learn how to customize the Medusa Admin to show a product's brand on its details page, and to show a new page with the list of brands in your store. -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`. +# Guide: Sync Brands from Medusa to CMS -*** +In the [previous chapter](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows. -## Request URLs with Trailing Backslashes +In another previous chapter, you [added a workflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well. -A middleware whose `matcher` pattern doesn't end with a backslash won't be applied for requests to URLs with a trailing backslash. +Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system. -For example, consider you have the following middleware: +Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). -```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports" -import { - MedusaNextFunction, - MedusaRequest, - MedusaResponse, - defineMiddlewares, -} from "@medusajs/framework/http" +In this chapter, you'll modify the `createBrandWorkflow` you created before to emit a custom event that indicates a brand was created. Then, you'll listen to that event in a subscriber to sync the brand to the third-party CMS. You'll implement the sync logic within a workflow that you execute in the subscriber. -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom", - middlewares: [ - ( - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - console.log("Received a request!") +### Prerequisites - next() - }, - ], - }, - ], -}) -``` +- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) +- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) -If you send a request to `http://localhost:9000/custom`, the middleware will run. +## 1. Emit Event in createBrandWorkflow -However, if you send a request to `http://localhost:9000/custom/`, the middleware won't run. +Since syncing the brand to the third-party system isn't integral to creating a brand, you'll emit a custom event indicating that a brand was created. -In general, avoid adding trailing backslashes when sending requests to API routes. +Medusa provides an `emitEventStep` that allows you to emit an event in your workflows. So, in the `createBrandWorkflow` defined in `src/workflows/create-brand.ts`, use the `emitEventStep` helper step after the `createBrandStep`: -*** +```ts title="src/workflows/create-brand.ts" highlights={eventHighlights} +// other imports... +import { + emitEventStep, +} from "@medusajs/medusa/core-flows" -## Middlewares and Route Ordering +// ... -The ordering explained in this section was added in [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6) +export const createBrandWorkflow = createWorkflow( + "create-brand", + (input: CreateBrandInput) => { + // ... -The Medusa application registers middlewares and API route handlers in the following order: + emitEventStep({ + eventName: "brand.created", + data: { + id: brand.id, + }, + }) -1. Global middlewares in the following order: - 1. Global middleware defined in the Medusa's core. - 2. Global middleware defined in the plugins (in the order the plugins are registered in). - 3. Global middleware you define in the application. -2. Route middlewares in the following order: - 1. Route middleware defined in the Medusa's core. - 2. Route middleware defined in the plugins (in the order the plugins are registered in). - 3. Route middleware you define in the application. -3. API routes in the following order: - 1. API routes defined in the Medusa's core. - 2. API routes defined in the plugins (in the order the plugins are registered in). - 3. API routes you define in the application. + return new WorkflowResponse(brand) + } +) +``` -### Middlewares Sorting +The `emitEventStep` accepts an object parameter having two properties: -On top of the previous ordering, Medusa sorts global and route middlewares based on their matcher pattern in the following order: +- `eventName`: The name of the event to emit. You'll use this name later to listen to the event in a subscriber. +- `data`: The data payload to emit with the event. This data is passed to subscribers that listen to the event. You add the brand's ID to the data payload, informing the subscribers which brand was created. -1. Wildcard matchers. For example, `/custom*`. -2. Regex matchers. For example, `/custom/(products|collections)`. -3. Static matchers without parameters. For example, `/custom`. -4. Static matchers with parameters. For example, `/custom/:id`. +You'll learn how to handle this event in a later step. -For example, if you have the following middlewares: +*** -```ts title="src/api/middlewares.ts" -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom/:id", - middlewares: [/* ... */], - }, - { - matcher: "/custom", - middlewares: [/* ... */], - }, - { - matcher: "/custom*", - method: ["GET"], - middlewares: [/* ... */], - }, - { - matcher: "/custom/:id", - method: ["GET"], - middlewares: [/* ... */], - }, - ], -}) -``` +## 2. Create Sync to Third-Party System Workflow -The global middlewares are sorted into the following order before they're registered: +The subscriber that will listen to the `brand.created` event will sync the created brand to the third-party CMS. So, you'll implement the syncing logic in a workflow, then execute the workflow in the subscriber. -1. Global middleware `/custom`. -2. Global middleware `/custom/:id`. +Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. -And the route middlewares are sorted into the following order before they're registered: +Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). -1. Route middleware `/custom*`. -2. Route middleware `/custom/:id`. +You'll create a `syncBrandToSystemWorkflow` that has two steps: -Then, the middlwares are registered in the order mentioned earlier, with global middlewares first, then the route middlewares. +- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll use this to retrieve the brand's details using its ID. +- `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS. -### Middlewares and Route Execution Order +### syncBrandToCmsStep -When a request is sent to an API route, the global middlewares are executed first, then the route middlewares, and finally the route handler. +To implement the step that syncs the brand to the CMS, create the file `src/workflows/sync-brands-to-cms.ts` with the following content: -For example, consider you have the following middlewares: +![Directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493547/Medusa%20Book/cms-dir-overview-4_u5t0ug.jpg) -```ts title="src/api/middlewares.ts" -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom", - middlewares: [ - (req, res, next) => { - console.log("Global middleware") - next() - }, - ], - }, - { - matcher: "/custom", - method: ["GET"], - middlewares: [ - (req, res, next) => { - console.log("Route middleware") - next() - }, - ], - }, - ], -}) -``` - -When you send a request to `/custom` route, the following messages are logged in the terminal: - -```bash -Global middleware -Route middleware -Hello from custom! # message logged from API route handler -``` - -The global middleware runs first, then the route middleware, and finally the route handler, assuming that it logs the message `Hello from custom!`. - -*** +```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { InferTypeOf } from "@medusajs/framework/types" +import { Brand } from "../modules/brand/models/brand" +import { CMS_MODULE } from "../modules/cms" +import CmsModuleService from "../modules/cms/service" -## Overriding Middlewares +type SyncBrandToCmsStepInput = { + brand: InferTypeOf +} -A middleware can not override an existing middleware. Instead, middlewares are added to the end of the middleware stack. +const syncBrandToCmsStep = createStep( + "sync-brand-to-cms", + async ({ brand }: SyncBrandToCmsStepInput, { container }) => { + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) -For example, if you define a custom validation middleware, such as `validateAndTransformBody`, on an existing route, then both the original and the custom validation middleware will run. + await cmsModuleService.createBrand(brand) + return new StepResponse(null, brand.id) + }, + async (id, { container }) => { + if (!id) { + return + } -# Protected Routes + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) -In this chapter, you’ll learn how to create protected routes. + await cmsModuleService.deleteBrand(id) + } +) +``` -## What is a Protected Route? +You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) and use its `createBrand` method. This method will create the brand in the third-party CMS. -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. +You also pass the brand's ID to the step's compensation function. In this function, you delete the brand in the third-party CMS if an error occurs during the workflow's execution. -*** +Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). -## Default Protected Routes +### Create Workflow -Medusa applies an authentication guard on routes starting with `/admin`, including custom API routes. +You can now create the workflow that uses the above step. Add the workflow to the same `src/workflows/sync-brands-to-cms.ts` file: -Requests to `/admin` must be user-authenticated to access the route. +```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncWorkflowHighlights} +// other imports... +import { + // ... + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -Refer to the API Reference for [Admin](https://docs.medusajs.com/api/admin#authentication) and [Store](https://docs.medusajs.com/api/store#authentication) authentication methods. +// ... -*** +type SyncBrandToCmsWorkflowInput = { + id: string +} -## Protect Custom API Routes +export const syncBrandToCmsWorkflow = createWorkflow( + "sync-brand-to-cms", + (input: SyncBrandToCmsWorkflowInput) => { + // @ts-ignore + const { data: brands } = useQueryGraphStep({ + entity: "brand", + fields: ["*"], + filters: { + id: input.id, + }, + options: { + throwIfKeyNotFound: true, + }, + }) -To protect custom API Routes to only allow authenticated customer or admin users, use the `authenticate` middleware from the Medusa Framework. + syncBrandToCmsStep({ + brand: brands[0], + } as SyncBrandToCmsStepInput) -For example: + return new WorkflowResponse({}) + } +) +``` -```ts title="src/api/middlewares.ts" highlights={highlights} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" +You create a `syncBrandToCmsWorkflow` that accepts the brand's ID as input. The workflow has the following steps: -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom/admin*", - middlewares: [authenticate("user", ["session", "bearer", "api-key"])], - }, - { - matcher: "/custom/customer*", - middlewares: [authenticate("customer", ["session", "bearer"])], - }, - ], -}) -``` +- `useQueryGraphStep`: Retrieve the brand's details using Query. You pass the brand's ID as a filter, and set the `throwIfKeyNotFound` option to true so that the step throws an error if a brand with the specified ID doesn't exist. +- `syncBrandToCmsStep`: Create the brand in the third-party CMS. -The `authenticate` middleware function accepts three parameters: +You'll execute this workflow in the subscriber next. -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 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 properties: - - `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. - - `allowUnregistered` (default: `false`): A boolean indicating if unregistered users should be allowed access. This is useful when you want to allow users who aren’t registered to access certain routes. +Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). *** -## Authentication Opt-Out +## 3. Handle brand.created Event -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`. +You now have a workflow with the logic to sync a brand to the CMS. You need to execute this workflow whenever the `brand.created` event is emitted. So, you'll create a subscriber that listens to and handle the event. -For example: +Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/brand-created.ts` with the following content: -```ts title="src/api/admin/custom/route.ts" highlights={[["15"]]} -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +![Directory structure of the Medusa application after adding the subscriber](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493774/Medusa%20Book/cms-dir-overview-5_iqqwvg.jpg) -export const GET = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "Hello", +```ts title="src/subscribers/brand-created.ts" highlights={subscriberHighlights} +import type { + SubscriberConfig, + SubscriberArgs, +} from "@medusajs/framework" +import { syncBrandToCmsWorkflow } from "../workflows/sync-brands-to-cms" + +export default async function brandCreatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await syncBrandToCmsWorkflow(container).run({ + input: data, }) } -export const AUTHENTICATE = false +export const config: SubscriberConfig = { + event: "brand.created", +} ``` -Now, any request sent to the `/admin/custom` API route is allowed, regardless if the admin user is authenticated. +A subscriber file must export: -*** +- The asynchronous function that's executed when the event is emitted. This must be the file's default export. +- An object that holds the subscriber's configurations. It has an `event` property that indicates the name of the event that the subscriber is listening to. -## Authenticated Request Type +The subscriber function accepts an object parameter that has two properties: -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`. +- `event`: An object of event details. Its `data` property holds the event's data payload, which is the brand's ID. +- `container`: The Medusa container used to resolve framework and commerce tools. -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`. +In the function, you execute the `syncBrandToCmsWorkflow`, passing it the data payload as an input. So, everytime a brand is created, Medusa will execute this function, which in turn executes the workflow to sync the brand to the CMS. -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. +Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). -### 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. +## Test it Out -For example: +To test the subscriber and workflow out, you'll use the [Create Brand API route](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md) you created in a previous chapter. -```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" +First, start the Medusa application: -export const GET = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - if (req.auth_context?.actor_id) { - // retrieve customer - const customerModuleService: ICustomerModuleService = req.scope.resolve( - Modules.CUSTOMER - ) +```bash npm2yarn +npm run dev +``` - const customer = await customerModuleService.retrieveCustomer( - req.auth_context.actor_id - ) - } +Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. So, to retrieve an authenticated token of your admin user, send 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" +}' ``` -In this example, you resolve the Customer Module's main service, then use it to retrieve the logged-in customer, if available. +Make sure to replace the email and password with your admin user's credentials. -### Retrieve Logged-In Admin User's Details +Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). -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. +Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: -For example: +```bash +curl -X POST 'http://localhost:9000/admin/brands' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "name": "Acme" +}' +``` -```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 - ) +This request returns the created brand. 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: - // ... +```plain +info: Processing brand.created which has 1 subscribers +http: POST /admin/brands ← - (200) - 16.418 ms +info: Sending a POST request to /brands. +info: Request Data: { + "id": "01JEDWENYD361P664WRQPMC3J8", + "name": "Acme", + "created_at": "2024-12-06T11:42:32.909Z", + "updated_at": "2024-12-06T11:42:32.909Z", + "deleted_at": null } +info: API Key: "123" ``` -In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user. +*** +## Next Chapter: Sync Brand from Third-Party CMS to Medusa -# Configure Request Body Parser +You can also automate syncing data from a third-party system to Medusa at a regular interval. In the next chapter, you'll learn how to sync brands from the third-party CMS to Medusa once a day. -In this chapter, you'll learn how to configure the request body parser for your API routes. -## Default Body Parser Configuration +# Guide: Schedule Syncing Brands from CMS -The Medusa application configures the body parser by default to parse JSON, URL-encoded, and text request content types. You can parse other data types by adding the relevant [Express middleware](https://expressjs.com/en/guide/using-middleware.html) or preserve the raw body data by configuring the body parser, which is useful for webhook requests. +In the previous chapters, you've [integrated a third-party CMS](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) and implemented the logic to [sync created brands](https://docs.medusajs.com/learn/customization/integrate-systems/handle-event/index.html.md) from Medusa to the CMS. -This chapter shares some examples of configuring the body parser for different data types or use cases. +However, when you integrate a third-party system, you want the data to be in sync between the Medusa application and the system. One way to do so is by automatically syncing the data once a day. -*** +You can create an action to be automatically executed at a specified interval using scheduled jobs. A scheduled job is an asynchronous function with a specified schedule of when the Medusa application should run it. Scheduled jobs are useful to automate repeated tasks. -## Preserve Raw Body Data for Webhooks +Learn more about scheduled jobs in [this chapter](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). -If your API route receives webhook requests, you might want to preserve the raw body data. To do this, you can configure the body parser to parse the raw body data and store it in the `req.rawBody` property. +In this chapter, you'll create a scheduled job that triggers syncing the brands from the third-party CMS to Medusa once a day. You'll implement the syncing logic in a workflow, and execute that workflow in the scheduled job. -To do that, create the file `src/api/middlewares.ts` with the following content: +### Prerequisites -```ts title="src/api/middlewares.ts" highlights={preserveHighlights} -import { defineMiddlewares } from "@medusajs/framework/http" +- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) -export default defineMiddlewares({ - routes: [ - { - method: ["POST"], - bodyParser: { preserveRawBody: true }, - matcher: "/custom", - }, - ], -}) -``` +*** -The middleware route object passed to `routes` accepts a `bodyParser` property whose value is an object of configuration for the default body parser. By enabling the `preserveRawBody` property, the raw body data is preserved and stored in the `req.rawBody` property. +## 1. Implement Syncing Workflow -Learn more about [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). +You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job. -You can then access the raw body data in your API route handler: +Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. -```ts title="src/api/custom/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - console.log(req.rawBody) +This workflow will have three steps: - // TODO use raw body -} -``` +1. `retrieveBrandsFromCmsStep` to retrieve the brands from the CMS. +2. `createBrandsStep` to create the brands retrieved in the first step that don't exist in Medusa. +3. `updateBrandsStep` to update the brands retrieved in the first step that exist in Medusa. -*** +### retrieveBrandsFromCmsStep -## Configure Request Body Size Limit +To create the step that retrieves the brands from the third-party CMS, create the file `src/workflows/sync-brands-from-cms.ts` with the following content: -By default, the body parser limits the request body size to `100kb`. If a request body exceeds that size, the Medusa application throws an error. +![Directory structure of the Medusa application after creating the file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494196/Medusa%20Book/cms-dir-overview-6_z1omsi.jpg) -You can configure the body parser to accept larger request bodies by setting the `sizeLimit` property of the `bodyParser` object in a middleware route object. For example: +```ts title="src/workflows/sync-brands-from-cms.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import CmsModuleService from "../modules/cms/service" +import { CMS_MODULE } from "../modules/cms" -```ts title="src/api/middlewares.ts" highlights={sizeLimitHighlights} -import { defineMiddlewares } from "@medusajs/framework/http" +const retrieveBrandsFromCmsStep = createStep( + "retrieve-brands-from-cms", + async (_, { container }) => { + const cmsModuleService: CmsModuleService = container.resolve( + CMS_MODULE + ) -export default defineMiddlewares({ - routes: [ - { - method: ["POST"], - bodyParser: { sizeLimit: "2mb" }, - matcher: "/custom", - }, - ], -}) + const brands = await cmsModuleService.retrieveBrands() + + return new StepResponse(brands) + } +) ``` -The `sizeLimit` property accepts one of the following types of values: +You create a `retrieveBrandsFromCmsStep` that resolves the CMS Module's service and uses its `retrieveBrands` method to retrieve the brands in the CMS. You return those brands in the step's response. -- A string representing the size limit in bytes (For example, `100kb`, `2mb`, `5gb`). It is passed to the [bytes](https://www.npmjs.com/package/bytes) library to parse the size. -- A number representing the size limit in bytes. For example, `1024` for 1kb. +### createBrandsStep -*** +The brands retrieved in the first step may have brands that don't exist in Medusa. So, you'll create a step that creates those brands. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: -## Configure File Uploads +```ts title="src/workflows/sync-brands-from-cms.ts" highlights={createBrandsHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" +// other imports... +import BrandModuleService from "../modules/brand/service" +import { BRAND_MODULE } from "../modules/brand" -To accept file uploads in your API routes, you can configure the [Express Multer middleware](https://expressjs.com/en/resources/middleware/multer.html) on your route. +// ... -The `multer` package is available through the `@medusajs/medusa` package, so you don't need to install it. However, for better typing support, install the `@types/multer` package as a development dependency: +type CreateBrand = { + name: string +} -```bash npm2yarn -npm install --save-dev @types/multer -``` +type CreateBrandsInput = { + brands: CreateBrand[] +} -Then, to configure file upload for your route, create the file `src/api/middlewares.ts` with the following content: +export const createBrandsStep = createStep( + "create-brands-step", + async (input: CreateBrandsInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) -```ts title="src/api/middlewares.ts" highlights={uploadHighlights} -import { defineMiddlewares } from "@medusajs/framework/http" -import multer from "multer" + const brands = await brandModuleService.createBrands(input.brands) -const upload = multer({ storage: multer.memoryStorage() }) + return new StepResponse(brands, brands) + }, + async (brands, { container }) => { + if (!brands) { + return + } -export default defineMiddlewares({ - routes: [ - { - method: ["POST"], - matcher: "/custom", - middlewares: [ - // @ts-ignore - upload.array("files"), - ], - }, - ], -}) + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + await brandModuleService.deleteBrands(brands.map((brand) => brand.id)) + } +) ``` -In the example above, you configure the `multer` middleware to store the uploaded files in memory. Then, you apply the `upload.array("files")` middleware to the route to accept file uploads. By using the `array` method, you accept multiple file uploads with the same `files` field name. +The `createBrandsStep` accepts the brands to create as an input. It resolves the [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)'s service and uses the generated `createBrands` method to create the brands. -You can then access the uploaded files in your API route handler: +The step passes the created brands to the compensation function, which deletes those brands if an error occurs during the workflow's execution. -```ts title="src/api/custom/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - const files = req.files as Express.Multer.File[] +### Update Brands Step - // TODO handle files +The brands retrieved in the first step may also have brands that exist in Medusa. So, you'll create a step that updates their details to match that of the CMS. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: + +```ts title="src/workflows/sync-brands-from-cms.ts" highlights={updateBrandsHighlights} +// ... + +type UpdateBrand = { + id: string + name: string } -``` -The uploaded files are stored in the `req.files` property as an array of Multer file objects that have properties like `filename` and `mimetype`. +type UpdateBrandsInput = { + brands: UpdateBrand[] +} -### Uploading Files using File Module Provider +export const updateBrandsStep = createStep( + "update-brands-step", + async ({ brands }: UpdateBrandsInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) -The recommended way to upload the files to storage using the configured [File Module Provider](https://docs.medusajs.com/resources/architectural-modules/file/index.html.md) is to use the [uploadFilesWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md): + const prevUpdatedBrands = await brandModuleService.listBrands({ + id: brands.map((brand) => brand.id), + }) -```ts title="src/api/custom/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { MedusaError } from "@medusajs/framework/utils" -import { uploadFilesWorkflow } from "@medusajs/medusa/core-flows" + const updatedBrands = await brandModuleService.updateBrands(brands) -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - const files = req.files as Express.Multer.File[] + return new StepResponse(updatedBrands, prevUpdatedBrands) + }, + async (prevUpdatedBrands, { container }) => { + if (!prevUpdatedBrands) { + return + } - if (!files?.length) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "No files were uploaded" + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE ) - } - - const { result } = await uploadFilesWorkflow(req.scope).run({ - input: { - files: files?.map((f) => ({ - filename: f.originalname, - mimeType: f.mimetype, - content: f.buffer.toString("binary"), - access: "public", - })), - }, - }) - res.status(200).json({ files: result }) -} + await brandModuleService.updateBrands(prevUpdatedBrands) + } +) ``` -Check out the [uploadFilesWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md) for details on the expected input and output of the workflow. +The `updateBrandsStep` receives the brands to update in Medusa. In the step, you retrieve the brand's details in Medusa before the update to pass them to the compensation function. You then update the brands using the Brand Module's `updateBrands` generated method. +In the compensation function, which receives the brand's old data, you revert the update using the same `updateBrands` method. -# Request Body and Query Parameter Validation - -In this chapter, you'll learn how to validate request body and query parameters in your custom API route. - -## Request Validation +### Create Workflow -Consider you're creating a `POST` API route at `/custom`. It accepts two parameters `a` and `b` that are required numbers, and returns their sum. +Finally, you'll create the workflow that uses the above steps to sync the brands from the CMS to Medusa. Add to the same `src/workflows/sync-brands-from-cms.ts` file the following: -Medusa provides two middlewares to validate the request body and query paramters of incoming requests to your custom API routes: +```ts title="src/workflows/sync-brands-from-cms.ts" +// other imports... +import { + // ... + createWorkflow, + transform, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -- `validateAndTransformBody` to validate the request's body parameters against a schema. -- `validateAndTransformQuery` to validate the request's query parameters against a schema. +// ... -Both middlewares accept a [Zod](https://zod.dev/) schema as a parameter, which gives you flexibility in how you define your validation schema with complex rules. +export const syncBrandsFromCmsWorkflow = createWorkflow( + "sync-brands-from-system", + () => { + const brands = retrieveBrandsFromCmsStep() -The next steps explain how to add request body and query parameter validation to the API route mentioned earlier. + // TODO create and update brands + } +) +``` -*** +In the workflow, you only use the `retrieveBrandsFromCmsStep` for now, which retrieves the brands from the third-party CMS. -## How to Validate Request Body +Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use [transform](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values. -### Step 1: Create Validation Schema +Learn more about data manipulation using `transform` in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). -Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. +So, replace the `TODO` with the following: -To create a validation schema with Zod, create a `validators.ts` file in any `src/api` subfolder. This file holds Zod schemas for each of your API routes. +```ts title="src/workflows/sync-brands-from-cms.ts" +const { toCreate, toUpdate } = transform( + { + brands, + }, + (data) => { + const toCreate: CreateBrand[] = [] + const toUpdate: UpdateBrand[] = [] -For example, create the file `src/api/custom/validators.ts` with the following content: + data.brands.forEach((brand) => { + if (brand.external_id) { + toUpdate.push({ + id: brand.external_id as string, + name: brand.name as string, + }) + } else { + toCreate.push({ + name: brand.name as string, + }) + } + }) -```ts title="src/api/custom/validators.ts" -import { z } from "zod" + return { toCreate, toUpdate } + } +) -export const PostStoreCustomSchema = z.object({ - a: z.number(), - b: z.number(), -}) +// TODO create and update the brands ``` -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. +`transform` accepts two parameters: -### Step 2: Add Request Body Validation Middleware +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. -To use this schema for validating the body parameters of requests to `/custom`, use the `validateAndTransformBody` middleware provided by `@medusajs/framework/http`. It accepts the Zod schema as a parameter. +In `transform`'s function, you loop over the brands array to check which should be created or updated. This logic assumes that a brand in the CMS has an `external_id` property whose value is the brand's ID in Medusa. -For example, create the file `src/api/middlewares.ts` with the following content: +You now have the list of brands to create and update. So, replace the new `TODO` with the following: -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - validateAndTransformBody, -} from "@medusajs/framework/http" -import { PostStoreCustomSchema } from "./custom/validators" +```ts title="src/workflows/sync-brands-from-cms.ts" +const created = createBrandsStep({ brands: toCreate }) +const updated = updateBrandsStep({ brands: toUpdate }) -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom", - method: "POST", - middlewares: [ - validateAndTransformBody(PostStoreCustomSchema), - ], - }, - ], +return new WorkflowResponse({ + created, + updated, }) ``` -This applies the `validateAndTransformBody` middleware on `POST` requests to `/custom`. It uses the `PostStoreCustomSchema` as the validation schema. +You first run the `createBrandsStep` to create the brands that don't exist in Medusa, then the `updateBrandsStep` to update the brands that exist in Medusa. You pass the arrays returned by `transform` as the inputs for the steps. -#### How the Validation Works +Finally, you return an object of the created and updated brands. You'll execute this workflow in the scheduled job next. -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`. +## 2. Schedule Syncing Task -### Step 3: Use Validated Body in API Route +You now have the workflow to sync the brands from the CMS to Medusa. Next, you'll create a scheduled job that runs this workflow once a day to ensure the data between Medusa and the CMS are always in sync. -In your API route, consume the validated body using the `validatedBody` property of `MedusaRequest`. +A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. So, create the file `src/jobs/sync-brands-from-cms.ts` with the following content: -For example, create the file `src/api/custom/route.ts` with the following content: +![Directory structure of the Medusa application after adding the scheduled job](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494592/Medusa%20Book/cms-dir-overview-7_dkjb9s.jpg) -```ts title="src/api/custom/route.ts" highlights={routeHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { z } from "zod" -import { PostStoreCustomSchema } from "./validators" +```ts title="src/jobs/sync-brands-from-cms.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { syncBrandsFromCmsWorkflow } from "../workflows/sync-brands-from-cms" -type PostStoreCustomSchemaType = z.infer< - typeof PostStoreCustomSchema -> +export default async function (container: MedusaContainer) { + const logger = container.resolve("logger") -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - sum: req.validatedBody.a + req.validatedBody.b, - }) -} -``` + const { result } = await syncBrandsFromCmsWorkflow(container).run() -In the API route, you use the `validatedBody` property of `MedusaRequest` to access the values of the `a` and `b` properties. + logger.info( + `Synced brands from third-party system: ${ + result.created.length + } brands created and ${result.updated.length} brands updated.`) +} -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. +export const config = { + name: "sync-brands-from-system", + schedule: "0 0 * * *", // change to * * * * * for debugging +} +``` -### Test it Out +A scheduled job file must export: -To test out the validation, send a `POST` request to `/custom` passing `a` and `b` body parameters. You can try sending incorrect request body parameters to test out the validation. +- An asynchronous function that will be executed at the specified schedule. This function must be the file's default export. +- An object of scheduled jobs configuration. It has two properties: + - `name`: A unique name for the scheduled job. + - `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. -For example, if you omit the `a` parameter, you'll receive a `400` response code with the following response data: +The scheduled job function accepts as a parameter the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) used to resolve framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated. -```json -{ - "type": "invalid_data", - "message": "Invalid request: Field 'a' is required" -} -``` +Based on the cron expression specified in `config.schedule`, Medusa will run the scheduled job every day at midnight. You can also change it to `* * * * *` to run it every minute for easier debugging. *** -## How to Validate Request Query Paramters +## Test it Out -The steps to validate the request query parameters are the similar to that of [validating the body](#how-to-validate-request-body). +To test out the scheduled job, start the Medusa application: -### Step 1: Create Validation Schema +```bash npm2yarn +npm run dev +``` -The first step is to create a schema with Zod with the rules of the accepted query parameters. +If you set the schedule to `* * * * *` for debugging, the scheduled job will run in a minute. You'll see in the logs how many brands were created or updated. -Consider that the API route accepts two query parameters `a` and `b` that are numbers, similar to the previous section. +*** -Create the file `src/api/custom/validators.ts` with the following content: +## Summary -```ts title="src/api/custom/validators.ts" -import { z } from "zod" +By following the previous chapters, you utilized Medusa's framework and orchestration tools to perform and automate tasks that span across systems. -export const PostStoreCustomSchema = z.object({ - a: z.preprocess( - (val) => { - if (val && typeof val === "string") { - return parseInt(val) - } - return val - }, - z - .number() - ), - b: z.preprocess( - (val) => { - if (val && typeof val === "string") { - return parseInt(val) - } - return val - }, - z - .number() - ), -}) -``` +With Medusa, you can integrate any service from your commerce ecosystem with ease. You don't have to set up separate applications to manage your different customizations, or worry about data inconsistency across systems. Your efforts only go into implementing the business logic that ties your systems together. -Since a query parameter's type is originally a string or array of strings, you have to use Zod's `preprocess` method to validate other query types, such as numbers. -For both `a` and `b`, you transform the query parameter's value to an integer first if it's a string, then, you check that the resulting value is a number. +# Admin Development Constraints -### Step 2: Add Request Query Validation Middleware +This chapter lists some constraints of admin widgets and UI routes. -Next, you'll use the schema to validate incoming requests' query parameters to the `/custom` API route. +## Arrow Functions -Add the `validateAndTransformQuery` middleware to the API route in the file `src/api/middlewares.ts`: +Widget and UI route components must be created as arrow functions. -```ts title="src/api/middlewares.ts" -import { - validateAndTransformQuery, - defineMiddlewares, -} from "@medusajs/framework/http" -import { PostStoreCustomSchema } from "./custom/validators" +```ts highlights={arrowHighlights} +// Don't +function ProductWidget() { + // ... +} -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom", - method: "POST", - middlewares: [ - validateAndTransformQuery( - PostStoreCustomSchema, - {} - ), - ], - }, - ], -}) +// Do +const ProductWidget = () => { + // ... +} ``` -The `validateAndTransformQuery` accepts two parameters: +*** -- The first one is the Zod schema to validate the query parameters against. -- The second one is an object of options for retrieving data using Query, which you can learn more about in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). +## Widget Zone -#### How the Validation Works +A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. -If a request's query parameters don't pass the validation, the `validateAndTransformQuery` middleware throws an error indicating the validation errors. - -If a request's query parameters are validated successfully, the middleware sets the validated query parameters in the `validatedQuery` property of `MedusaRequest`. - -### Step 3: Use Validated Query in API Route - -Finally, use the validated query in the API route. The `MedusaRequest` parameter has a `validatedQuery` parameter that you can use to access the validated parameters. - -For example, create the file `src/api/custom/route.ts` with the following content: - -```ts title="src/api/custom/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +```ts highlights={zoneHighlights} +// Don't +export const config = defineWidgetConfig({ + zone: `product.details.before`, +}) -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const a = req.validatedQuery.a as number - const b = req.validatedQuery.b as number +// Don't +const ZONE = "product.details.after" +export const config = defineWidgetConfig({ + zone: ZONE, +}) - res.json({ - sum: a + b, - }) -} +// Do +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) ``` -In the API route, you use the `validatedQuery` property of `MedusaRequest` to access the values of the `a` and `b` properties as numbers, then return in the response their sum. - -### Test it Out - -To test out the validation, send a `POST` request to `/custom` with `a` and `b` query parameters. You can try sending incorrect query parameters to see how the validation works. -For example, if you omit the `a` parameter, you'll receive a `400` response code with the following response data: +# Guide: Integrate CMS Brand System -```json -{ - "type": "invalid_data", - "message": "Invalid request: Field 'a' is required" -} -``` +In the previous chapters, you've created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that adds brands to your application. In this chapter, you'll integrate a dummy Content-Management System (CMS) in a new module. The module's service will provide methods to retrieve and manage brands in the CMS. You'll later use this service to sync data from and to the CMS. -*** +Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -## Learn More About Validation Schemas +## 1. Create Module Directory -To see different examples and learn more about creating a validation schema, refer to [Zod's documentation](https://zod.dev). +You'll integrate the third-party system in a new CMS Module. So, create the directory `src/modules/cms` that will hold the module's resources. +![Directory structure after adding the directory for the CMS Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492447/Medusa%20Book/cms-dir-overview-1_gasguk.jpg) -# API Route Response +*** -In this chapter, you'll learn how to send a response in your API route. +## 2. Create Module Service -## Send a JSON Response +Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system. -To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler. +Create the CMS Module's service at `src/modules/cms/service.ts` with the following content: -For example: +![Directory structure after adding the CMS Module's service](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492583/Medusa%20Book/cms-dir-overview-2_zwcwh3.jpg) -```ts title="src/api/custom/route.ts" highlights={jsonHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +```ts title="src/modules/cms/service.ts" highlights={serviceHighlights} +import { Logger, ConfigModule } from "@medusajs/framework/types" -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "Hello, World!", - }) +export type ModuleOptions = { + apiKey: string } -``` - -This API route returns the following JSON object: -```json -{ - "message": "Hello, World!" +type InjectedDependencies = { + logger: Logger + configModule: ConfigModule } -``` -*** +class CmsModuleService { + private options_: ModuleOptions + private logger_: Logger -## Set Response Status Code + constructor({ logger }: InjectedDependencies, options: ModuleOptions) { + this.logger_ = logger + this.options_ = options -By default, setting the JSON data using the `json` method returns a response with a `200` status code. + // TODO initialize SDK + } +} -To change the status code, use the `status` method of the `MedusaResponse` object. +export default CmsModuleService +``` -For example: +You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters: -```ts title="src/api/custom/route.ts" highlights={statusHighlight} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +1. The module's container. Since a module is [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), it has a [local container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) different than the Medusa container you use in other customizations. This container holds framework tools like the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) and resources within the module. +2. Options passed to the module when it's later added in Medusa's configurations. These options are useful to pass secret keys or configurations that ensure your module is re-usable across applications. For the CMS Module, you accept the API key to connect to the dummy CMS as an option. -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.status(201).json({ - message: "Hello, World!", - }) -} -``` +When integrating a third-party system that has a Node.js SDK or client, you can initialize that client in the constructor to be used in the service's methods. -The response of this API route has the status code `201`. +### Integration Methods -*** +Next, you'll add methods that simulate sending requests to a third-party CMS. You'll use these methods later to sync brands from and to the CMS. -## Change Response Content Type +Add the following methods in the `CmsModuleService`: -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. +```ts title="src/modules/cms/service.ts" highlights={methodsHighlights} +export class CmsModuleService { + // ... -For example, to create an API route that returns an event stream: + // 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}.`) + this.logger_.info(`Request Data: ${JSON.stringify(data, null, 2)}`) + this.logger_.info(`API Key: ${JSON.stringify(this.options_.apiKey, null, 2)}`) + } -```ts highlights={streamHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + async createBrand(brand: Record) { + await this.sendRequest("/brands", "POST", brand) + } -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }) + async deleteBrand(id: string) { + await this.sendRequest(`/brands/${id}`, "DELETE") + } - const interval = setInterval(() => { - res.write("Streaming data...\n") - }, 3000) + async retrieveBrands(): Promise[]> { + await this.sendRequest("/brands", "GET") - req.on("end", () => { - clearInterval(interval) - res.end() - }) + return [] + } } ``` -The `writeHead` method accepts two parameters: +The `sendRequest` method sends requests to the third-party CMS. Since this guide isn't using a real CMS, it only simulates the sending by logging messages in the terminal. -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. +You also add three methods that use the `sendRequest` method: -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. +- `createBrand` that creates a brand in the third-party system. +- `deleteBrand` that deletes the brand in the third-party system. +- `retrieveBrands` to retrieve a brand from the third-party system. *** -## Do More with Responses +## 3. Export Module Definition -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. +After creating the module's service, you'll export the module definition indicating the module's name and service. +Create the file `src/modules/cms/index.ts` with the following content: -# Event Data Payload +![Directory structure of the Medusa application after adding the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492991/Medusa%20Book/cms-dir-overview-3_b0byks.jpg) -In this chapter, you'll learn how subscribers receive an event's data payload. +```ts title="src/modules/cms/index.ts" +import { Module } from "@medusajs/framework/utils" +import CmsModuleService from "./service" -## Access Event's Data Payload +export const CMS_MODULE = "cms" -When events are emitted, they’re emitted with a data payload. +export default Module(CMS_MODULE, { + service: CmsModuleService, +}) +``` -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. +You use `Module` from the Modules SDK to export the module's defintion, indicating that the module's name is `cms` and its service is `CmsModuleService`. -For example: +*** -```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" +## 4. Add Module to Medusa's Configurations -export default async function productCreateHandler({ - event, -}: SubscriberArgs<{ id: string }>) { - const productId = event.data.id - console.log(`The product ${productId} was created`) -} +Finally, add the module to the Medusa configurations at `medusa-config.ts`: -export const config: SubscriberConfig = { - event: "product.created", -} +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + // ... + { + resolve: "./src/modules/cms", + options: { + apiKey: process.env.CMS_API_KEY, + }, + }, + ], +}) ``` -The `event` object has the following properties: +The object passed in `modules` accept an `options` property, whose value is an object of options to pass to the module. These are the options you receive in the `CmsModuleService`'s constructor. -- data: (\`object\`) The data payload of the event. Its properties are different for each event. -- name: (string) The name of the triggered event. -- metadata: (\`object\`) Additional data and context of the emitted event. +You can add the `CMS_API_KEY` environment variable to `.env`: -This logs the product ID received in the `product.created` event’s data payload to the console. +```bash +CMS_API_KEY=123 +``` -{/* --- +*** -## List of Events with Data Payload +## Next Steps: Sync Brand From Medusa to CMS -Refer to [this reference](!resources!/events-reference) for a full list of events emitted by Medusa and their data payloads. */} +You can now use the CMS Module's service to perform actions on the third-party CMS. +In the next chapter, you'll learn how to emit an event when a brand is created, then handle that event to sync the brand from Medusa to the third-party service. -# Seed Data with Custom CLI Script -In this chapter, you'll learn how to seed data using a custom CLI script. +# Environment Variables in Admin Customizations -## How to Seed Data +In this chapter, you'll learn how to use environment variables in your admin customizations. -To seed dummy data for development or demo purposes, use a custom CLI script. +To learn how envirnment variables are generally loaded in Medusa based on your application's environment, check out [this chapter](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md). -In the CLI script, use your custom workflows or Medusa's existing workflows, which you can browse in [this reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md), to seed data. +## How to Set Environment Variables -### Example: Seed Dummy Products +The Medusa Admin is built on top of [Vite](https://vite.dev/). To set an environment variable that you want to use in a widget or UI route, prefix the environment variable with `VITE_`. -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: +For example: -```bash npm2yarn -npm install --save-dev @faker-js/faker +```plain +VITE_MY_API_KEY=sk_123 ``` -Then, create the file `src/scripts/demo-products.ts` with the following content: +*** -```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" +## How to Use Environment Variables -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 - ) +To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object. - const defaultSalesChannel = await salesChannelModuleService - .listSalesChannels({ - name: "Default Sales Channel", - }) +For example: - const sizeOptions = ["S", "M", "L", "XL"] - const colorOptions = ["Black", "White"] - const currency_code = "eur" - const productsNum = 50 +```tsx highlights={[["8"]]} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" - // TODO seed products +const ProductWidget = () => { + return ( + +
+ API Key: {import.meta.env.VITE_MY_API_KEY} +
+
+ ) } -``` - -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)], - }, - })), - shipping_profile_id: "sp_123", - sales_channels: [ - { - id: defaultSalesChannel[0].id, - }, - ], - } +export const config = defineWidgetConfig({ + zone: "product.details.before", }) -// TODO seed products +export default ProductWidget ``` -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: +In this example, you display the API key in a widget using `import.meta.env.VITE_MY_API_KEY`. -```ts title="src/scripts/demo-products.ts" -const { result: products } = await createProductsWorkflow(container).run({ - input: { - products: productsData, - }, -}) +### Type Error on import.meta.env -logger.info(`Seeded ${products.length} products.`) +If you receive a type error on `import.meta.env`, create the file `src/admin/vite-env.d.ts` with the following content: -// TODO add inventory levels +```ts title="src/admin/vite-env.d.ts" +/// ``` -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.") +This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables. -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: ["id"], -}) +*** -const { data: inventoryItems } = await query.graph({ - entity: "inventory_item", - fields: ["id"], -}) +## Check Node Environment in Admin Customizations -const inventoryLevels = inventoryItems.map((inventoryItem) => ({ - location_id: stockLocations[0].id, - stocked_quantity: 1000000, - inventory_item_id: inventoryItem.id, -})) +To check the current environment, Vite exposes two variables: -await createInventoryLevelsWorkflow(container).run({ - input: { - inventory_levels: inventoryLevels, - }, -}) +- `import.meta.env.DEV`: Returns `true` if the current environment is development. +- `import.meta.env.PROD`: Returns `true` if the current environment is production. -logger.info("Finished seeding inventory levels data.") -``` +Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode). -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. +# Admin Widgets -Finally, you use the `createInventoryLevelsWorkflow` from Medusa's core workflows to create the inventory levels. +In this chapter, you’ll learn more about widgets and how to use them. -### Test Script +## What is an Admin Widget? -To test out the script, run the following command in your project's directory: +The Medusa Admin dashboard's pages are customizable to insert widgets of custom content in pre-defined injection zones. You create these widgets as React components that allow admin users to perform custom actions. -```bash -npx medusa exec ./src/scripts/demo-products.ts -``` +For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. -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. +*** +## How to Create a Widget? -# Add Data Model Check Constraints +### Prerequisites -In this chapter, you'll learn how to add check constraints to your data model. +- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) -## What is a Check Constraint? +You create a widget in a `.tsx` file under the `src/admin/widgets` directory. The file’s default export must be the widget, which is the React component that renders the custom content. The file must also export the widget’s configurations indicating where to insert the widget. -A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown. +For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: -For example, if you have a data model with a `price` property, you want to only allow positive number values. So, you add a check constraint that fails when inserting a record with a negative price value. +![Example of widget file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867137/Medusa%20Book/widget-dir-overview_dqsbct.jpg) -*** +```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" -## How to Set a Check Constraint? +// The widget +const ProductWidget = () => { + return ( + +
+ Product Widget +
+
+ ) +} -To set check constraints on a data model, use the `checks` method. This method accepts an array of check constraints to apply on the data model. +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) -For example, to set a check constraint on a `price` property that ensures its value can only be a positive number: +export default ProductWidget +``` -```ts highlights={checks1Highlights} -import { model } from "@medusajs/framework/utils" +You export the `ProductWidget` component, which shows the heading `Product Widget`. In the widget, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md), a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it. -const CustomProduct = model.define("custom_product", { - // ... - price: model.bigNumber(), -}) -.checks([ - (columns) => `${columns.price} >= 0`, -]) -``` +To export the widget's configurations, you use `defineWidgetConfig` from the Admin Extension SDK. It accepts as a parameter an object with the `zone` property, whose value is a string or an array of strings, each being the name of the zone to inject the widget into. -The item passed in the array parameter of `checks` can be a callback function that accepts as a parameter an object whose keys are the names of the properties in the data model schema, and values the respective column name in the database. +In the example above, the widget is injected at the top of a product’s details. -The function returns a string indicating the [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). In the expression, use the `columns` parameter to access a property's column name. +The widget component must be created as an arrow function. -You can also pass an object to the `checks` method: +### Test the Widget -```ts highlights={checks2Highlights} -import { model } from "@medusajs/framework/utils" +To test out the widget, start the Medusa application: -const CustomProduct = model.define("custom_product", { - // ... - price: model.bigNumber(), -}) -.checks([ - { - name: "custom_product_price_check", - expression: (columns) => `${columns.price} >= 0`, - }, -]) +```bash npm2yarn +npm run dev ``` -The object accepts the following properties: - -- `name`: The check constraint's name. -- `expression`: A function similar to the one that can be passed to the array. It accepts an object of columns and returns an [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). +Then, open a product’s details page. You’ll find your custom widget at the top of the page. *** -## Apply in Migrations +## Props Passed in Detail Pages -After adding the check constraint, make sure to generate and run migrations if you already have the table in the database. Otherwise, the check constraint won't be reflected. - -To generate a migration for the data model's module then reflect it on the database, run the following command: +Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. -```bash -npx medusa db:generate custom_module -npx medusa db:migrate -``` +For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: -The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. +```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) => { + return ( + +
+ + Product Widget {data.title} + +
+
+ ) +} -# Emit Workflow and Service Events +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) -In this chapter, you'll learn about event types and how to emit an event in a service or workflow. +export default ProductWidget +``` -## Event Types +The props type is `DetailWidgetProps`, and it accepts as a type argument the expected type of `data`. For the product details page, it's `AdminProduct`. -In your customization, you can emit an event, then listen to it in a subscriber and perform an asynchronus action, such as send a notification or data to a third-party system. +*** -There are two types of events in Medusa: +## Injection Zone -1. Workflow event: an event that's emitted in a workflow after a commerce feature is performed. For example, Medusa emits the `order.placed` event after a cart is completed. -2. Service event: an event that's emitted to track, trace, or debug processes under the hood. For example, you can emit an event with an audit trail. +Refer to [this reference](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md) for the full list of injection zones and their props. -### Which Event Type to Use? +*** -**Workflow events** are the most common event type in development, as most custom features and customizations are built around workflows. +## Admin Components List -Some examples of workflow events: +To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. -1. When a user creates a blog post and you're emitting an event to send a newsletter email. -2. When you finish syncing products to a third-party system and you want to notify the admin user of new products added. -3. When a customer purchases a digital product and you want to generate and send it to them. -You should only go for a **service event** if you're emitting an event for processes under the hood that don't directly affect front-facing features. +# Admin Routing Customizations -Some examples of service events: +The Medusa Admin dashboard uses [React Router](https://reactrouter.com) under the hood to manage routing. So, you can have more flexibility in routing-related customizations using some of React Router's utilities, hooks, and components. -1. When you're tracing data manipulation and changes, and you want to track every time some custom data is changed. -2. When you're syncing data with a search engine. +In this chapter, you'll learn about routing-related customizations that you can use in your admin customizations using React Router. -*** +`react-router-dom` is available in your project by default through the Medusa packages. You don't need to install it separately. -## Emit Event in a Workflow +## Link to a Page -To emit a workflow event, use the `emitEventStep` helper step provided in the `@medusajs/medusa/core-flows` package. +To link to a page in your admin customizations, you can use the `Link` component from `react-router-dom`. For example: -For example: +```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" -```ts highlights={highlights} -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - emitEventStep, -} from "@medusajs/medusa/core-flows" +// The widget +const ProductWidget = () => { + return ( + + View Orders + + ) +} -const helloWorldWorkflow = createWorkflow( - "hello-world", - () => { - // ... +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) - emitEventStep({ - eventName: "custom.created", - data: { - id: "123", - // other data payload - }, - }) - } -) +export default ProductWidget ``` -The `emitEventStep` accepts an object having the following properties: - -- `eventName`: The event's name. -- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. - -In this example, you emit the event `custom.created` and pass in the data payload an ID property. +This adds a widget to a product's details page with a link to the Orders page. The link's path must be without the `/app` prefix. -### Test it Out +*** -If you execute the workflow, the event is emitted and you can see it in your application's logs. +## Admin Route Loader -Any subscribers listening to the event are executed. +Route loaders are available starting from Medusa v2.5.1. -*** +In your UI route or any other custom admin route, you may need to retrieve data to use it in your route component. For example, you may want to fetch a list of products to display on a custom page. -## Emit Event in a Service +To do that, you can export a `loader` function in the route file, which is a [React Router loader](https://reactrouter.com/6.29.0/route/loader#loader). In this function, you can fetch and return data asynchronously. Then, in your route component, you can use the [useLoaderData](https://reactrouter.com/6.29.0/hooks/use-loader-data#useloaderdata) hook from React Router to access the data. -To emit a service event: +For example, consider the following UI route created at `src/admin/routes/custom/page.tsx`: -1. Resolve `event_bus` from the module's container in your service's constructor: +```tsx title="src/admin/routes/custom/page.tsx" highlights={loaderHighlights} +import { Container, Heading } from "@medusajs/ui" +import { + useLoaderData, +} from "react-router-dom" -### Extending Service Factory +export async function loader() { + // TODO fetch products -```ts title="src/modules/hello/service.ts" highlights={["9"]} -import { IEventBusService } from "@medusajs/framework/types" -import { MedusaService } from "@medusajs/framework/utils" + return { + products: [], + } +} -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - protected eventBusService_: AbstractEventBusModuleService +const CustomPage = () => { + const { products } = useLoaderData() as Awaited> - constructor({ event_bus }) { - super(...arguments) - this.eventBusService_ = event_bus - } + return ( +
+ +
+ Products count: {products.length} +
+
+
+ ) } + +export default CustomPage ``` -### Without Service Factory +In this example, you first export a `loader` function that can be used to fetch data, such as products. The function returns an object with a `products` property. -```ts title="src/modules/hello/service.ts" highlights={["6"]} -import { IEventBusService } from "@medusajs/framework/types" +Then, in the `CustomPage` route component, you use the `useLoaderData` hook from React Router to access the data returned by the `loader` function. You can then use the data in your component. -class HelloModuleService { - protected eventBusService_: AbstractEventBusModuleService +### Route Parameters - constructor({ event_bus }) { - this.eventBusService_ = event_bus - } -} -``` +You can also access route params in the loader function. For example, consider the following UI route created at `src/admin/routes/custom/[id]/page.tsx`: -2. Use the event bus service's `emit` method in the service's methods to emit an event: +```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={loaderParamHighlights} +import { Container, Heading } from "@medusajs/ui" +import { + useLoaderData, + LoaderFunctionArgs, +} from "react-router-dom" -```ts title="src/modules/hello/service.ts" highlights={serviceHighlights} -class HelloModuleService { - // ... - performAction() { - // TODO perform action +export async function loader({ params }: LoaderFunctionArgs) { + const { id } = params + // TODO fetch product by id - this.eventBusService_.emit({ - name: "custom.event", - data: { - id: "123", - // other data payload - }, - }) + return { + id, } } -``` - -The method accepts an object having the following properties: -- `name`: The event's name. -- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. - -3. By default, the Event Module's service isn't injected into your module's container. To add it to the container, pass it in the module's registration object in `medusa-config.ts` in the `dependencies` property: +const CustomPage = () => { + const { id } = useLoaderData() as Awaited> -```ts title="medusa-config.ts" highlights={depsHighlight} -import { Modules } from "@medusajs/framework/utils" + return ( +
+ +
+ Product ID: {id} +
+
+
+ ) +} -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/hello", - dependencies: [ - Modules.EVENT_BUS, - ], - }, - ], -}) +export default CustomPage ``` -The `dependencies` property accepts an array of module registration keys. The specified modules' main services are injected into the module's container. +Because the UI route has a route parameter `[id]`, you can access the `id` parameter in the `loader` function. The loader function accepts as a parameter an object of type `LoaderFunctionArgs` from React Router. This object has a `params` property that contains the route parameters. -That's how you can resolve it in your module's main service's constructor. +In the loader, you can fetch data asynchronously using the route parameter and return it. Then, in the route component, you can access the data using the `useLoaderData` hook. -### Test it Out +### When to Use Route Loaders -If you execute the `performAction` method of your service, the event is emitted and you can see it in your application's logs. +A route loader is executed before the route is loaded. So, it will block navigation until the loader function is resolved. -Any subscribers listening to the event are also executed. +Only use route loaders when the route component needs data essential before rendering. Otherwise, use the JS SDK with Tanstack (React) Query as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md). This way, you can fetch data asynchronously and update the UI when the data is available. You can also use a loader to prepare some initial data that's used in the route component before the data is retrieved. +*** -# Data Model Default Properties +## Other React Router Utilities -In this chapter, you'll learn about the properties available by default in your data model. +### Route Handles -When you create a data model, the following properties are created for you by Medusa: +Route handles are available starting from Medusa v2.5.1. -- `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. +In your UI route or any route file, you can export a `handle` object to define [route handles](https://reactrouter.com/start/framework/route-module#handle). The object is passed to the loader and route contexts. +For example: -# Configure Data Model Properties +```tsx title="src/admin/routes/custom/page.tsx" +export const handle = { + sandbox: true, +} +``` -In this chapter, you’ll learn how to configure data model properties. +### React Router Components and Hooks -## Property’s Default Value +Refer to [react-router-dom’s documentation](https://reactrouter.com/en/6.29.0) for components and hooks that you can use in your admin customizations. -Use the `default` method on a property's definition to specify the default value of a property. -For example: +# Admin UI Routes -```ts highlights={defaultHighlights} -import { model } from "@medusajs/framework/utils" +In this chapter, you’ll learn how to create a UI route in the admin dashboard. -const MyCustom = model.define("my_custom", { - color: model - .enum(["black", "white"]) - .default("black"), - age: model - .number() - .default(0), - // ... -}) +## What is a UI Route? -export default MyCustom -``` +The Medusa Admin dashboard is customizable, allowing you to add new pages, called UI routes. You create a UI route as a React component showing custom content that allow admin users to perform custom actions. -In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`. +For example, you can add a new page to show and manage product reviews, which aren't available natively in Medusa. *** -## Nullable Property +## How to Create a UI Route? -Use the `nullable` method to indicate that a property’s value can be `null`. +### Prerequisites -For example: +- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) -```ts highlights={nullableHighlights} -import { model } from "@medusajs/framework/utils" +You create a UI route in a `page.tsx` file under a sub-directory of `src/admin/routes` directory. The file's path relative to `src/admin/routes` determines its path in the dashboard. The file’s default export must be the UI route’s React component. -const MyCustom = model.define("my_custom", { - price: model.bigNumber().nullable(), - // ... -}) +For example, create the file `src/admin/routes/custom/page.tsx` with the following content: -export default MyCustom -``` +![Example of UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867243/Medusa%20Book/ui-route-dir-overview_tgju25.jpg) -*** +```tsx title="src/admin/routes/custom/page.tsx" +import { Container, Heading } from "@medusajs/ui" -## Unique Property +const CustomPage = () => { + return ( + +
+ This is my custom route +
+
+ ) +} -The `unique` method indicates that a property’s value must be unique in the database through a unique index. +export default CustomPage +``` -For example: +You add a new route at `http://localhost:9000/app/custom`. The `CustomPage` component holds the page's content, which currently only shows a heading. -```ts highlights={uniqueHighlights} -import { model } from "@medusajs/framework/utils" +In the route, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md), a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it. -const User = model.define("user", { - email: model.text().unique(), - // ... -}) +The UI route component must be created as an arrow function. -export default User -``` +### Test the UI Route -In this example, multiple users can’t have the same email. +To test the UI route, start the Medusa application: +```bash npm2yarn +npm run dev +``` -# Data Model Database Index +Then, after logging into the admin dashboard, open the page `http://localhost:9000/app/custom` to see your custom page. -In this chapter, you’ll learn how to define a database index on a data model. +*** -## Define Database Index on Property +## Show UI Route in the Sidebar -Use the `index` method on a property's definition to define a database index. +To add a sidebar item for your custom UI route, export a configuration object in the UI route's file: -For example: +```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" -```ts highlights={highlights} -import { model } from "@medusajs/framework/utils" +const CustomPage = () => { + return ( + +
+ This is my custom route +
+
+ ) +} -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text().index( - "IDX_MY_CUSTOM_NAME" - ), +export const config = defineRouteConfig({ + label: "Custom Route", + icon: ChatBubbleLeftRight, }) -export default MyCustom +export default CustomPage ``` -The `index` method optionally accepts the name of the index as a parameter. +The configuration object is created using `defineRouteConfig` from the Medusa Framework. It accepts the following properties: -In this example, you define an index on the `name` property. +- `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](https://docs.medusajs.com/ui/icons/overview/index.html.md). -## Define Database Index on Data Model +### Nested UI Routes -A data model has an `indexes` method that defines database indices on its properties. +Consider that along the UI route above at `src/admin/routes/custom/page.tsx` you create a nested UI route at `src/admin/routes/custom/nested/page.tsx` that also exports route configurations: -The index can be on multiple columns (composite index). For example: +![Example of nested UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867243/Medusa%20Book/ui-route-dir-overview_tgju25.jpg) -```ts highlights={dataModelIndexHighlights} -import { model } from "@medusajs/framework/utils" +```tsx title="src/admin/routes/custom/nested/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - }, -]) +const NestedCustomPage = () => { + return ( + +
+ This is my nested custom route +
+
+ ) +} -export default MyCustom +export const config = defineRouteConfig({ + label: "Nested Route", +}) + +export default NestedCustomPage ``` -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. +This UI route is shown in the sidebar as an item nested in the parent "Custom Route" item. Nested items are only shown when the parent sidebar items (in this case, "Custom Route") are clicked. -In the above example, you define a composite index on the `name` and `age` properties. +#### Caveats -### Index Conditions +Some caveats for nested UI routes in the sidebar: -An index can have conditions. For example: +- Nested dynamic UI routes, such as one created at `src/admin/routes/custom/[id]/page.tsx` aren't added to the sidebar as it's not possible to link to a dynamic route. If the dynamic route exports route configurations, a warning is logged in the browser's console. +- Nested routes in setting pages aren't shown in the sidebar to follow the admin's design conventions. +- The `icon` configuration is ignored for the sidebar item of nested UI route to follow the admin's design conventions. -```ts highlights={conditionHighlights} -import { model } from "@medusajs/framework/utils" +### Route Under Existing Admin Route -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - where: { - age: 30, - }, - }, -]) +You can add a custom UI route under an existing route. For example, you can add a route under the orders route: -export default MyCustom -``` +```tsx title="src/admin/routes/orders/nested/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" -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. +const NestedOrdersPage = () => { + return ( + +
+ Nested Orders Page +
+
+ ) +} -In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`. +export const config = defineRouteConfig({ + label: "Nested Orders", + nested: "/orders", +}) -A property's condition can be a negation. For example: +export default NestedOrdersPage +``` -```ts highlights={negationHighlights} -import { model } from "@medusajs/framework/utils" +The `nested` property passed to `defineRouteConfig` specifies which route this custom route is nested under. This route will now show in the sidebar under the existing "Orders" sidebar item. -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 -``` +## Create Settings Page -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. +To create a page under the settings section of the admin dashboard, create a UI route under the path `src/admin/routes/settings`. -In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`. +For example, create a UI route at `src/admin/routes/settings/custom/page.tsx`: -### Unique Database Index +![Example of settings UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867435/Medusa%20Book/setting-ui-route-dir-overview_kytbh8.jpg) -The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index. +```tsx title="src/admin/routes/settings/custom/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" -For example: +const CustomSettingPage = () => { + return ( + +
+ Custom Setting Page +
+
+ ) +} -```ts highlights={uniqueHighlights} -import { model } from "@medusajs/framework/utils" +export const config = defineRouteConfig({ + label: "Custom", +}) -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 +export default CustomSettingPage ``` -This creates a unique composite index on the `name` and `age` properties. +This adds a page under the path `/app/settings/custom`. An item is also added to the settings sidebar with the label `Custom`. +*** -# Infer Type of Data Model +## Path Parameters -In this chapter, you'll learn how to infer the type of a data model. +A UI route can accept path parameters if the name of any of the directories in its path is of the format `[param]`. -## How to Infer Type of Data Model? +For example, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content: -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. +![Example of UI route file with path parameters in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867748/Medusa%20Book/path-param-ui-route-dir-overview_kcfbev.jpg) -Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. +```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, Heading } from "@medusajs/ui" -For example: +const CustomPage = () => { + const { id } = useParams() -```ts -import { InferTypeOf } from "@medusajs/framework/types" -import { MyCustom } from "../models/my-custom" // relative path to the model + return ( + +
+ Passed ID: {id} +
+
+ ) +} -export type MyCustom = InferTypeOf +export default CustomPage ``` -`InferTypeOf` accepts as a type argument the type of the data model. +You access the passed parameter using `react-router-dom`'s [useParams hook](https://reactrouter.com/en/main/hooks/use-params). -Since the `MyCustom` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. +If you run the Medusa application and go to `localhost:9000/app/custom/123`, you'll see `123` printed in the page. -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" +## Admin Components List -type MyCustom = InferTypeOf +To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. -class HelloModuleService extends MedusaService({ MyCustom }) { - async doSomething(): Promise { - // ... - } -} -``` +*** +## More Routes Customizations -# Manage Relationships +For more customizations related to routes, refer to the [Routing Customizations chapter](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md). -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 +# Admin Development Tips -### BelongsTo Side of One-to-One +In this chapter, you'll find some tips for your admin development. -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}_id` property, where `{relation}` is the name of the relation property. +## Send Requests to API Routes -For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set an email's user ID as follows: +To send a request to an API route in the Medusa Application, use Medusa's [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) with [Tanstack Query](https://tanstack.com/query/latest). Both of these tools are installed in your project by default. -```ts highlights={belongsHighlights} -// when creating an email -const email = await helloModuleService.createEmails({ - // other properties... - user_id: "123", -}) +Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. -// when updating an email -const email = await helloModuleService.updateEmails({ - id: "321", - // other properties... - user_id: "123", +First, create the file `src/admin/lib/config.ts` to setup the SDK for use in your customizations: + +```ts +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + baseUrl: import.meta.env.VITE_BACKEND_URL || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, }) ``` -In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to. +Notice that you use `import.meta.env` to access environment variables in your customizations, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). -### HasOne Side +Learn more about the JS SDK's configurations [this documentation](https://docs.medusajs.com/resources/js-sdk#js-sdk-configurations/index.html.md). -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. +Then, use the configured SDK with the `useQuery` Tanstack Query hook to send `GET` requests, and `useMutation` hook to send `POST` or `DELETE` requests. -For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set a user's email ID as follows: +For example: -```ts highlights={hasOneHighlights} -// when creating a user -const user = await helloModuleService.createUsers({ - // other properties... - email: "123", -}) +### Query -// when updating a user -const user = await helloModuleService.updateUsers({ - id: "321", - // other properties... - email: "123", -}) -``` +```tsx title="src/admin/widgets/product-widget.ts" highlights={queryHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Button, Container } from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { sdk } from "../lib/config" +import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" -In the example above, you pass the `email` property when creating or updating a user to specify the email it has. +const ProductWidget = () => { + const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list(), + queryKey: ["products"], + }) + + return ( + + {isLoading && Loading...} + {data?.products && ( +
    + {data.products.map((product) => ( +
  • {product.title}
  • + ))} +
+ )} +
+ ) +} -*** +export const config = defineWidgetConfig({ + zone: "product.list.before", +}) -## Manage One-to-Many Relationship +export default ProductWidget +``` -In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. +### Mutation -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. +```tsx title="src/admin/widgets/product-widget.ts" highlights={mutationHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Button, Container } from "@medusajs/ui" +import { useMutation } from "@tanstack/react-query" +import { sdk } from "../lib/config" +import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" -For example, assuming you have the [Product and Store data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-many-relationship/index.html.md), set a product's store ID as follows: +const ProductWidget = ({ + data: productData, +}: DetailWidgetProps) => { + const { mutateAsync } = useMutation({ + mutationFn: (payload: HttpTypes.AdminUpdateProduct) => + sdk.admin.product.update(productData.id, payload), + onSuccess: () => alert("updated product"), + }) -```ts highlights={manyBelongsHighlights} -// when creating a product -const product = await helloModuleService.createProducts({ - // other properties... - store_id: "123", -}) + const handleUpdate = () => { + mutateAsync({ + title: "New Product Title", + }) + } + + return ( + + + + ) +} -// when updating a product -const product = await helloModuleService.updateProducts({ - id: "321", - // other properties... - store_id: "123", +export const config = defineWidgetConfig({ + zone: "product.details.before", }) + +export default ProductWidget ``` -In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to. +You can also send requests to custom routes as explained in the [JS SDK reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). -*** +### Use Route Loaders for Initial Data -## Manage Many-to-Many Relationship +You may need to retrieve data before your component is rendered, or you may need to pass some initial data to your component to be used while data is being fetched. In those cases, you can use a [route loader](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md). -If your many-to-many relation is represented with a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship-with-pivotentity) instead. +*** -### Create Associations +## Global Variables in Admin Customizations -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. +In your admin customizations, you can use the following global variables: -For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), set the association between products and orders as follows: +- `__BASE__`: The base path of the Medusa Admin, as set in the [admin.path](https://docs.medusajs.com/learn/configurations/medusa-config#path/index.html.md) configuration in `medusa-config.ts`. +- `__BACKEND_URL__`: The URL to the Medusa backend, as set in the [admin.backendUrl](https://docs.medusajs.com/learn/configurations/medusa-config#backendurl/index.html.md) configuration in `medusa-config.ts`. +- `__STOREFRONT_URL__`: The URL to the storefront, as set in the [admin.storefrontUrl](https://docs.medusajs.com/learn/configurations/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`. -```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"], -}) -``` +## Admin Translations -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. +The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions. -### Update Associations +Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/learn/resources/contribution-guidelines/admin-translations/index.html.md). -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. +# Seed Data with Custom CLI Script -For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you update the product's related orders as so: +In this chapter, you'll learn how to seed data using a custom CLI script. -```ts -const product = await helloModuleService.updateProducts({ - id: "123", - // other properties... - orders: ["321"], -}) -``` +## How to Seed Data -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. +To seed dummy data for development or demo purposes, use a custom CLI script. -So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product: +In the CLI script, use your custom workflows or Medusa's existing workflows, which you can browse in [this reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md), to seed data. -```ts highlights={updateAssociationHighlights} -const product = await helloModuleService.retrieveProduct( - "123", - { - relations: ["orders"], - } -) +### Example: Seed Dummy Products -const updatedProduct = await helloModuleService.updateProducts({ - id: product.id, - // other properties... - orders: [ - ...product.orders.map((order) => order.id), - "321", - ], -}) -``` +In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products. -This keeps existing associations between the product and orders, and adds a new one. +First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script: -*** +```bash npm2yarn +npm install --save-dev @faker-js/faker +``` -## Manage Many-to-Many Relationship with pivotEntity +Then, create the file `src/scripts/demo-products.ts` with the following content: -If your many-to-many relation is represented without a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship) instead. +```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" -If you have a many-to-many relation with a `pivotEntity` specified, make sure to pass the data model representing the pivot table to [MedusaService](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that your module's service extends. +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 + ) -For example, assuming you have the [Order, Product, and OrderProduct models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), add `OrderProduct` to `MedusaService`'s object parameter: + const defaultSalesChannel = await salesChannelModuleService + .listSalesChannels({ + name: "Default Sales Channel", + }) -```ts highlights={["4"]} -class HelloModuleService extends MedusaService({ - Order, - Product, - OrderProduct, -}) {} + const sizeOptions = ["S", "M", "L", "XL"] + const colorOptions = ["Black", "White"] + const currency_code = "eur" + const productsNum = 50 + + // TODO seed products +} ``` -This will generate Create, Read, Update and Delete (CRUD) methods for the `OrderProduct` data model, which you can use to create relations between orders and products and manage the extra columns in the pivot table. +So far, in the script, you: -For example: +- 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. -```ts -// create order-product association -const orderProduct = await helloModuleService.createOrderProducts({ - order_id: "123", - product_id: "123", - metadata: { - test: true, - }, -}) +Next, replace the `TODO` with the following: -// update order-product association -const orderProduct = await helloModuleService.updateOrderProducts({ - id: "123", - metadata: { - test: false, - }, +```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)], + }, + })), + shipping_profile_id: "sp_123", + sales_channels: [ + { + id: defaultSalesChannel[0].id, + }, + ], + } }) -// delete order-product association -await helloModuleService.deleteOrderProducts("123") +// TODO seed products ``` -Since the `OrderProduct` data model belongs to the `Order` and `Product` data models, you can set its order and product as explained in the [one-to-many relationship section](#manage-one-to-many-relationship) using `order_id` and `product_id`. - -Refer to the [service factory reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for a full list of generated methods and their usages. - -*** - -## Retrieve Records of Relation +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. -The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options. +Then, replace the new `TODO` with the following: -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. +```ts title="src/scripts/demo-products.ts" +const { result: products } = await createProductsWorkflow(container).run({ + input: { + products: productsData, + }, +}) -For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you retrieve a product's orders as follows: +logger.info(`Seeded ${products.length} products.`) -```ts highlights={retrieveHighlights} -const product = await helloModuleService.retrieveProducts( - "123", - { - relations: ["orders"], - } -) +// TODO add inventory levels ``` -In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. - - -# Data Model’s Primary Key +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. -In this chapter, you’ll learn how to configure the primary key of a data model. +Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following: -## primaryKey Method +```ts title="src/scripts/demo-products.ts" +logger.info("Seeding inventory levels.") -To set any `id`, `text`, or `number` property as a primary key, use the `primaryKey` method. +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: ["id"], +}) -For example: +const { data: inventoryItems } = await query.graph({ + entity: "inventory_item", + fields: ["id"], +}) -```ts highlights={highlights} -import { model } from "@medusajs/framework/utils" +const inventoryLevels = inventoryItems.map((inventoryItem) => ({ + location_id: stockLocations[0].id, + stocked_quantity: 1000000, + inventory_item_id: inventoryItem.id, +})) -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - // ... +await createInventoryLevelsWorkflow(container).run({ + input: { + inventory_levels: inventoryLevels, + }, }) -export default MyCustom +logger.info("Finished seeding inventory levels data.") ``` -In the example above, the `id` property is defined as the data model's primary key. - - -# Data Model Property Types - -In this chapter, you’ll learn about the types of properties in a data model’s schema. - -## id +You use Query to retrieve the stock location, to use the first location in the application, and the inventory items. -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. +Then, you generate inventory levels for each inventory item, associating it with the first stock location. -For example: +Finally, you use the `createInventoryLevelsWorkflow` from Medusa's core workflows to create the inventory levels. -```ts highlights={idHighlights} -import { model } from "@medusajs/framework/utils" +### Test Script -const MyCustom = model.define("my_custom", { - id: model.id(), - // ... -}) +To test out the script, run the following command in your project's directory: -export default MyCustom +```bash +npx medusa exec ./src/scripts/demo-products.ts ``` -*** - -## text - -The `text` method defines a string property. - -For example: +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. -```ts highlights={textHighlights} -import { model } from "@medusajs/framework/utils" -const MyCustom = model.define("my_custom", { - name: model.text(), - // ... -}) +# Pass Additional Data to Medusa's API Route -export default MyCustom -``` +In this chapter, you'll learn how to pass additional data in requests to Medusa's API Route. -*** +## Why Pass Additional Data? -## number +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. -The `number` method defines a number property. +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: +For example, the [Create Product API Route](https://docs.medusajs.com/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. -```ts highlights={numberHighlights} -import { model } from "@medusajs/framework/utils" +### API Routes Accepting Additional Data -const MyCustom = model.define("my_custom", { - age: model.number(), - // ... -}) +### API Routes List -export default MyCustom -``` +- Campaigns + - [Create Campaign](https://docs.medusajs.com/api/admin#campaigns_postcampaigns) + - [Update Campaign](https://docs.medusajs.com/api/admin#campaigns_postcampaignsid) +- Cart + - [Create Cart](https://docs.medusajs.com/api/store#carts_postcarts) + - [Update Cart](https://docs.medusajs.com/api/store#carts_postcartsid) +- Collections + - [Create Collection](https://docs.medusajs.com/api/admin#collections_postcollections) + - [Update Collection](https://docs.medusajs.com/api/admin#collections_postcollectionsid) +- Customers + - [Create Customer](https://docs.medusajs.com/api/admin#customers_postcustomers) + - [Update Customer](https://docs.medusajs.com/api/admin#customers_postcustomersid) + - [Create Address](https://docs.medusajs.com/api/admin#customers_postcustomersidaddresses) + - [Update Address](https://docs.medusajs.com/api/admin#customers_postcustomersidaddressesaddress_id) +- Draft Orders + - [Create Draft Order](https://docs.medusajs.com/api/admin#draft-orders_postdraftorders) +- Orders + - [Complete Orders](https://docs.medusajs.com/api/admin#orders_postordersidcomplete) + - [Cancel Order's Fulfillment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idcancel) + - [Create Shipment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idshipments) + - [Create Fulfillment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillments) +- Products + - [Create Product](https://docs.medusajs.com/api/admin#products_postproducts) + - [Update Product](https://docs.medusajs.com/api/admin#products_postproductsid) + - [Create Product Variant](https://docs.medusajs.com/api/admin#products_postproductsidvariants) + - [Update Product Variant](https://docs.medusajs.com/api/admin#products_postproductsidvariantsvariant_id) + - [Create Product Option](https://docs.medusajs.com/api/admin#products_postproductsidoptions) + - [Update Product Option](https://docs.medusajs.com/api/admin#products_postproductsidoptionsoption_id) +- Product Tags + - [Create Product Tag](https://docs.medusajs.com/api/admin#product-tags_postproducttags) + - [Update Product Tag](https://docs.medusajs.com/api/admin#product-tags_postproducttagsid) +- Product Types + - [Create Product Type](https://docs.medusajs.com/api/admin#product-types_postproducttypes) + - [Update Product Type](https://docs.medusajs.com/api/admin#product-types_postproducttypesid) +- Promotions + - [Create Promotion](https://docs.medusajs.com/api/admin#promotions_postpromotions) + - [Update Promotion](https://docs.medusajs.com/api/admin#promotions_postpromotionsid) *** -## float +## How to Pass Additional Data -This property is only available after [Medusa v2.1.2](https://github.com/medusajs/medusa/releases/tag/v2.1.2). +### 1. Specify Validation of Additional Data -The `float` method defines a number property that allows for values with decimal places. +Before passing custom data in the `additional_data` object parameter, you must specify validation rules for the allowed properties in the object. -Use this property type when it's less important to have high precision for numbers with large decimal places. Alternatively, for higher percision, use the [bigNumber property](#bignumber). +To do that, use the middleware route object defined in `src/api/middlewares.ts`. -For example: +For example, create the file `src/api/middlewares.ts` with the following content: -```ts highlights={floatHighlights} -import { model } from "@medusajs/framework/utils" +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/framework/http" +import { z } from "zod" -const MyCustom = model.define("my_custom", { - rating: model.float(), - // ... +export default defineMiddlewares({ + routes: [ + { + method: "POST", + matcher: "/admin/products", + additionalDataValidator: { + brand: z.string().optional(), + }, + }, + ], }) - -export default MyCustom ``` -*** - -## bigNumber +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. -The `bigNumber` method defines a number property that expects large numbers, such as prices. +In this example, you indicate that the `additional_data` parameter accepts a `brand` property whose value is an optional string. -Use this property type when it's important to have high precision for numbers with large decimal places. Alternatively, for less percision, use the [float property](#float). +Refer to [Zod's documentation](https://zod.dev) for all available validation rules. -For example: +### 2. Pass the Additional Data in a Request -```ts highlights={bigNumberHighlights} -import { model } from "@medusajs/framework/utils" +You can now pass a `brand` property in the `additional_data` parameter of a request to the Create Product API Route. -const MyCustom = model.define("my_custom", { - price: model.bigNumber(), - // ... -}) +For example: -export default MyCustom +```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"] + } + ], + "shipping_profile_id": "{shipping_profile_id}", + "additional_data": { + "brand": "Acme" + } +}' ``` -*** +Make sure to replace the `{token}` in the authorization header with an admin user's authentication token, and `{shipping_profile_id}` with an existing shipping profile's ID. -## boolean +In this request, you pass in the `additional_data` parameter a `brand` property and set its value to `Acme`. -The `boolean` method defines a boolean property. +The `additional_data` is then passed to hooks in the `createProductsWorkflow` used by the API route. -For example: +*** -```ts highlights={booleanHighlights} -import { model } from "@medusajs/framework/utils" +## Use Additional Data in a Hook -const MyCustom = model.define("my_custom", { - hasAccount: model.boolean(), - // ... -}) +Learn about workflow hooks in [this guide](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). -export default MyCustom -``` +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. -### enum +To do that, create the file `src/workflows/hooks/product-created.ts` with the following content: -The `enum` method defines a property whose value can only be one of the specified values. +```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" -For example: +createProductsWorkflow.hooks.productsCreated( + async ({ products, additional_data }, { container }) => { + if (!additional_data?.brand) { + return + } -```ts highlights={enumHighlights} -import { model } from "@medusajs/framework/utils" + const productModuleService = container.resolve( + Modules.PRODUCT + ) -const MyCustom = model.define("my_custom", { - color: model.enum(["black", "white"]), - // ... -}) + await productModuleService.upsertProducts( + products.map((product) => ({ + ...product, + metadata: { + ...product.metadata, + brand: additional_data.brand, + }, + })) + ) -export default MyCustom + return new StepResponse(products, { + products, + additional_data, + }) + } +) ``` -The `enum` method accepts an array of possible string values. +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. -## dateTime +### Compensation Function -The `dateTime` method defines a timestamp property. +Hooks also accept a compensation function as a second parameter to undo the actions made by the step function. -For example: +For example, pass the following second parameter to the `productsCreated` hook: -```ts highlights={dateTimeHighlights} -import { model } from "@medusajs/framework/utils" +```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 MyCustom = model.define("my_custom", { - date_of_birth: model.dateTime(), - // ... -}) + const productModuleService = container.resolve( + Modules.PRODUCT + ) -export default MyCustom + await productModuleService.upsertProducts( + products + ) + } +) ``` -*** +This updates the products to their original state before adding the brand to their `metadata` property. -## json -The `json` method defines a property whose value is a stringified JSON object. +# HTTP Methods -For example: +In this chapter, you'll learn about how to add new API routes for each HTTP method. -```ts highlights={jsonHighlights} -import { model } from "@medusajs/framework/utils" +## HTTP Method Handler -const MyCustom = model.define("my_custom", { - metadata: model.json(), - // ... -}) +An API route is created for every HTTP method you export a handler function for in a route file. -export default MyCustom -``` +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: -## array +```ts title="src/api/hello-world/route.ts" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -The `array` method defines an array of strings property. +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "[GET] Hello world!", + }) +} -For example: +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "[POST] Hello world!", + }) +} +``` -```ts highlights={arrHightlights} -import { model } from "@medusajs/framework/utils" +This adds two API Routes: -const MyCustom = model.define("my_custom", { - names: model.array(), - // ... -}) +- A `GET` route at `http://localhost:9000/hello-world`. +- A `POST` route at `http://localhost:9000/hello-world`. -export default MyCustom -``` -*** +# Throwing and Handling Errors -## Properties Reference +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. -Refer to the [Data Model API reference](https://docs.medusajs.com/resources/references/data-model/index.html.md) for a full reference of the properties. +## Throw MedusaError +When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError` from the Medusa Framework. -# Data Model Relationships +The Medusa application's API route error handler then wraps your thrown error in a uniform object and returns it in the response. -In this chapter, you’ll learn how to define relationships between data models in your module. +For example: -## What is a Relationship Property? +```ts +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { MedusaError } from "@medusajs/framework/utils" -A relationship property defines an association in the database between two models. It's created using the Data Model Language (DML) methods, such as `hasOne` or `belongsTo`. +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." + ) + } -When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type. + // ... +} +``` -You want to create a relation between data models in the same module. +The `MedusaError` class accepts in its constructor two parameters: -You want to create a relationship between data models in different modules. Use module links instead. +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 -## One-to-One Relationship +The error object returned in the response has two properties: -A one-to-one relationship indicates that one record of a data model belongs to or is associated with another. +- `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. -To define a one-to-one relationship, create relationship properties in the data models using the following methods: +### MedusaError Types -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. +|Type|Description|Status Code| +|---|---|---|---|---| +|\`DB\_ERROR\`|Indicates a database error.|\`500\`| +|\`DUPLICATE\_ERROR\`|Indicates a duplicate of a record already exists. For example, when trying to create a customer whose email is registered by another customer.|\`422\`| +|\`INVALID\_ARGUMENT\`|Indicates an error that occurred due to incorrect arguments or other unexpected state.|\`500\`| +|\`INVALID\_DATA\`|Indicates a validation error.|\`400\`| +|\`UNAUTHORIZED\`|Indicates that a user is not authorized to perform an action or access a route.|\`401\`| +|\`NOT\_FOUND\`|Indicates that the requested resource, such as a route or a record, isn't found.|\`404\`| +|\`NOT\_ALLOWED\`|Indicates that an operation isn't allowed.|\`400\`| +|\`CONFLICT\`|Indicates that a request conflicts with another previous or ongoing request. The error message in this case is ignored for a default message.|\`409\`| +|\`PAYMENT\_AUTHORIZATION\_ERROR\`|Indicates an error has occurred while authorizing a payment.|\`422\`| +|Other error types|Any other error type results in an |\`500\`| -For example: +*** -```ts highlights={oneToOneHighlights} -import { model } from "@medusajs/framework/utils" +## Override Error Handler -const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email), -}) +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. -const Email = model.define("email", { - id: model.id().primaryKey(), - user: model.belongsTo(() => User, { - mappedBy: "email", - }), +This error handler will also be used for errors thrown in Medusa's API routes and resources. + +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.", + }) + }, }) ``` -In the example above, a user has one email, and an email belongs to one user. +The `errorHandler` property's value is a function that accepts four parameters: -The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model. +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. -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. +This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message. -### Optional Relationship -To make the relationship optional on the `hasOne` or `belongsTo` side, use the `nullable` method on either property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/configure-properties#nullable-property/index.html.md). +# Handling CORS in API Routes -### One-sided One-to-One Relationship +In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes. -If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. +## CORS Overview -For example: +Cross-Origin Resource Sharing (CORS) allows only configured origins to access your API Routes. -```ts highlights={oneToOneUndefinedHighlights} -import { model } from "@medusajs/framework/utils" +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. -const User = model.define("user", { - id: model.id().primaryKey(), -}) +### CORS Configurations -const Email = model.define("email", { - id: model.id().primaryKey(), - user: model.belongsTo(() => User, { - mappedBy: undefined, - }), +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", + // ... + }, + }, }) ``` -### One-to-One Relationship in the Database +This allows the `http://localhost:7001` origin to access the Admin API Routes, and the `http://localhost:8000` origin to access Store API Routes. -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: +Learn more about the CORS configurations in [this resource guide](https://docs.medusajs.com/learn/configurations/medusa-config#http/index.html.md). -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. +*** -![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) +## 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`. -## One-to-Many Relationship +For example: -A one-to-many relationship indicates that one record of a data model has many records of another data model. +```ts title="src/api/store/custom/route.ts" highlights={[["15"]]} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -To define a one-to-many relationship, create relationship properties in the data models using the following methods: +export const GET = ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "[GET] Hello world!", + }) +} -1. `hasMany`: indicates that the model has more than one record of the specified model. -2. `belongsTo`: indicates that the model belongs to one record of the specified model. +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: -```ts highlights={oneToManyHighlights} -import { model } from "@medusajs/framework/utils" +```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-10" expandButtonLabel="Show Imports" +import { defineMiddlewares } from "@medusajs/framework/http" +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" -const Store = model.define("store", { - id: model.id().primaryKey(), - products: model.hasMany(() => Product), -}) +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom*", + middlewares: [ + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + const configModule: ConfigModule = + req.scope.resolve("configModule") -const Product = model.define("product", { - id: model.id().primaryKey(), - store: model.belongsTo(() => Store, { - mappedBy: "products", - }), + return cors({ + origin: parseCorsOrigins( + configModule.projectConfig.http.storeCors + ), + credentials: true, + })(req, res, next) + }, + ], + }, + ], }) ``` -In this example, a store has many products, but a product belongs to one store. +This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`. -### Optional Relationship -To make the relationship optional on the `belongsTo` side, use the `nullable` method on the property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/configure-properties#nullable-property/index.html.md). +# Middlewares -### One-to-Many Relationship in the Database +In this chapter, you’ll learn about middlewares and how to create them. -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: +## What is a Middleware? -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. +A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler function. -![Diagram illustrating the relation between a store and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733937/Medusa%20Book/one-to-many_d6wtcw.jpg) +Middlwares are used to guard API routes, parse request content types other than `application/json`, manipulate request data, and more. -*** +As Medusa's server is based on Express, you can use any [Express middleware](https://expressjs.com/en/resources/middleware.html). -## Many-to-Many Relationship +### Middleware Types -A many-to-many relationship indicates that many records of a data model can be associated with many records of another data model. +There are two types of middlewares: -To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method. +1. Global Middleware: A middleware that applies to all routes matching a specified pattern. +2. Route Middleware: A middleware that applies to routes matching a specified pattern and HTTP method(s). -For example: +These middlewares generally have the same definition and usage, but they differ in the routes they apply to. You'll learn how to create both types in the following sections. -```ts highlights={manyToManyHighlights} -import { model } from "@medusajs/framework/utils" - -const Order = model.define("order", { - id: model.id().primaryKey(), - products: model.manyToMany(() => Product, { - mappedBy: "orders", - pivotTable: "order_product", - joinColumn: "order_id", - inverseJoinColumn: "product_id", - }), -}) - -const Product = model.define("product", { - id: model.id().primaryKey(), - orders: model.manyToMany(() => Order, { - mappedBy: "products", - }), -}) -``` - -The `manyToMany` method accepts two parameters: - -1. A function that returns the associated data model. -2. An object of optional configuration. Only one of the data models in the relation can define the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations, and it's considered the owner data model. The object can accept the following properties: - - `mappedBy`: The name of the relationship property in the other data model. If not set, the property's name is inferred from the associated data model's name. - - `pivotTable`: The name of the pivot table created in the database for the many-to-many relation. If not set, the pivot table is inferred by combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`. - - `joinColumn`: The name of the column in the pivot table that points to the owner model's primary key. - - `inverseJoinColumn`: The name of the column in the pivot table that points to the owned model's primary key. - -The `pivotTable`, `joinColumn`, and `inverseJoinColumn` properties are only available after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). - -Following [Medusa v2.1.0](https://github.com/medusajs/medusa/releases/tag/v2.1.0), if `pivotTable`, `joinColumn`, and `inverseJoinColumn` aren't specified on either model, the owner is decided based on alphabetical order. So, in the example above, the `Order` data model would be the owner. - -In this example, an order is associated with many products, and a product is associated with many orders. Since the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations are defined on the order, it's considered the owner data model. +*** -### Many-to-Many Relationship in the Database +## How to Create a Global Middleware? -When you generate the migrations of data models that have a many-to-many relationship, the migration adds a new pivot table. Its name is either the name you specify in the `pivotTable` configuration or the inferred name combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`. +Middlewares of all types are defined in the special file `src/api/middlewares.ts`. Use the `defineMiddlewares` function from the Medusa Framework to define the middlewares, and export its value. -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. +For example: -The pivot table has columns with foreign keys pointing to the primary key of the associated tables. The column's name is either: +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + MedusaNextFunction, + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -- The value of the `joinColumn` configuration for the owner table, and the `inverseJoinColumn` configuration for the owned table; -- Or the inferred name `{table_name}_id`. +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom*", + middlewares: [ + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + console.log("Received a request!") -![Diagram illustrating the relation between order and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726734269/Medusa%20Book/many-to-many_fzy5pq.jpg) + next() + }, + ], + }, + ], +}) +``` -### Many-To-Many with Custom Columns +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: -To add custom columns to the pivot table between two data models having a many-to-many relationship, you must define a new data model that represents the pivot table. +- `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 global and route middleware functions. -For example: +In the example above, you define a global middleware that logs the message `Received a request!` whenever a request is sent to an API route path starting with `/custom`. -```ts highlights={manyToManyColumnHighlights} -import { model } from "@medusajs/framework/utils" +### Test the Global Middleware -export const Order = model.define("order_test", { - id: model.id().primaryKey(), - products: model.manyToMany(() => Product, { - pivotEntity: () => OrderProduct, - }), -}) +To test the middleware: -export const Product = model.define("product_test", { - id: model.id().primaryKey(), - orders: model.manyToMany(() => Order), -}) +1. Start the application: -export const OrderProduct = model.define("orders_products", { - id: model.id().primaryKey(), - order: model.belongsTo(() => Order, { - mappedBy: "products", - }), - product: model.belongsTo(() => Product, { - mappedBy: "orders", - }), - metadata: model.json().nullable(), -}) +```bash npm2yarn +npm run dev ``` -The `Order` and `Product` data models have a many-to-many relationship. To add extra columns to the created pivot table, you pass a `pivotEntity` option to the `products` relation in `Order` (since `Order` is the owner). The value of `pivotEntity` is a function that returns the data model representing the pivot table. - -The `OrderProduct` model defines, aside from the ID, the following properties: +2. Send a request to any API route starting with `/custom`. +3. See the following message in the terminal: -- `order`: A relation that indicates this model belongs to the `Order` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Order` data model. -- `product`: A relation that indicates this model belongs to the `Product` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Product` data model. -- `metadata`: An extra column to add to the pivot table of type `json`. You can add other columns as well to the model. +```bash +Received a request! +``` *** -## 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 from that of the associated data model. +## How to Create a Route Middleware? -As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. +In the previous section, you learned how to create a global middleware. You define the route middleware in the same way in `src/api/middlewares.ts`, but you specify an additional property `method` in the middleware route object. Its value is one or more HTTP methods to apply the middleware to. For example: -```ts highlights={relationNameHighlights} -import { model } from "@medusajs/framework/utils" +```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, + defineMiddlewares, +} from "@medusajs/framework/http" -const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email, { - mappedBy: "owner", - }), -}) +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom*", + method: ["POST", "PUT"], + middlewares: [ + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + console.log("Received a request!") -const Email = model.define("email", { - id: model.id().primaryKey(), - owner: model.belongsTo(() => User, { - mappedBy: "email", - }), + next() + }, + ], + }, + ], }) ``` -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. +This example applies the middleware only when a `POST` or `PUT` request is sent to an API route path starting with `/custom`, changing the middleware from a global middleware to a route middleware. -For example, if a store is deleted, its products should also be deleted. +### Test the Route Middleware -The `cascades` method used on a data model configures which child records an operation is cascaded to. +To test the middleware: -For example: +1. Start the application: -```ts highlights={highlights} -import { model } from "@medusajs/framework/utils" +```bash npm2yarn +npm run dev +``` -const Store = model.define("store", { - id: model.id().primaryKey(), - products: model.hasMany(() => Product), -}) -.cascades({ - delete: ["products"], -}) +2. Send a `POST` request to any API route starting with `/custom`. +3. See the following message in the terminal: -const Product = model.define("product", { - id: model.id().primaryKey(), - store: model.belongsTo(() => Store, { - mappedBy: "products", - }), -}) +```bash +Received a request! ``` -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. +## When to Use Middlewares +- You want to protect API routes by a custom condition. +- You're modifying the request body. -# Searchable Data Model Property +*** -In this chapter, you'll learn what a searchable property is and how to define it. +## Middleware Function Parameters -## What is a Searchable Property? +The middleware function accepts three parameters: -Methods generated by the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that accept filters, such as `list{ModelName}s`, accept a `q` property as part of the filters. +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. -When the `q` filter is passed, the data model's searchable properties are queried to find matching records. +You must call the `next` function in the middleware. Otherwise, other middlewares and the API route handler won’t execute. *** -## Define a Searchable Property +## Middleware for Routes with Path Parameters -Use the `searchable` method on a `text` property to indicate that it's searchable. +To indicate a path parameter in a middleware's `matcher` pattern, use the format `:{param-name}`. For example: -```ts highlights={searchableHighlights} -import { model } from "@medusajs/framework/utils" +```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports" highlights={pathParamHighlights} +import { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, + defineMiddlewares, +} from "@medusajs/framework/http" -const MyCustom = model.define("my_custom", { - name: model.text().searchable(), - // ... +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/:id", + middlewares: [ + // ... + ], + }, + ], }) - -export default MyCustom ``` -In this example, the `name` property is searchable. +This applies a middleware to the routes defined in the file `src/api/custom/[id]/route.ts`. -### Search Example +*** -If you pass a `q` filter to the `listMyCustoms` method: +## Request URLs with Trailing Backslashes -```ts -const myCustoms = await helloModuleService.listMyCustoms({ - q: "John", -}) -``` +A middleware whose `matcher` pattern doesn't end with a backslash won't be applied for requests to URLs with a trailing backslash. -This retrieves records that include `John` in their `name` property. +For example, consider you have the following middleware: +```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports" +import { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, + defineMiddlewares, +} from "@medusajs/framework/http" -# Write Migration +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom", + middlewares: [ + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + console.log("Received a request!") -In this chapter, you'll learn how to create a migration and write it manually. + next() + }, + ], + }, + ], +}) +``` -## What is a Migration? +If you send a request to `http://localhost:9000/custom`, the middleware will run. -A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It has two methods: +However, if you send a request to `http://localhost:9000/custom/`, the middleware won't run. -- The `up` method reflects changes on the database. -- The `down` method reverts the changes made in the `up` method. +In general, avoid adding trailing backslashes when sending requests to API routes. *** -## How to Write a Migration? +## Middlewares and Route Ordering -The Medusa CLI tool provides a [db:generate](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbgenerate/index.html.md) command to generate a migration for the specified modules' data models. +The ordering explained in this section was added in [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6) -Alternatively, you can manually create a migration file under the `migrations` directory of your module. +The Medusa application registers middlewares and API route handlers in the following order: -For example: +1. Global middlewares in the following order: + 1. Global middleware defined in the Medusa's core. + 2. Global middleware defined in the plugins (in the order the plugins are registered in). + 3. Global middleware you define in the application. +2. Route middlewares in the following order: + 1. Route middleware defined in the Medusa's core. + 2. Route middleware defined in the plugins (in the order the plugins are registered in). + 3. Route middleware you define in the application. +3. API routes in the following order: + 1. API routes defined in the Medusa's core. + 2. API routes defined in the plugins (in the order the plugins are registered in). + 3. API routes you define in the application. -```ts title="src/modules/blog/migrations/Migration20240429.ts" -import { Migration } from "@mikro-orm/migrations" +### Middlewares Sorting -export class Migration20240702105919 extends Migration { +On top of the previous ordering, Medusa sorts global and route middlewares based on their matcher pattern in the following order: - async up(): Promise { - this.addSql("create table if not exists \"author\" (\"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 \"author_pkey\" primary key (\"id\"));") - } +1. Wildcard matchers. For example, `/custom*`. +2. Regex matchers. For example, `/custom/(products|collections)`. +3. Static matchers without parameters. For example, `/custom`. +4. Static matchers with parameters. For example, `/custom/:id`. - async down(): Promise { - this.addSql("drop table if exists \"author\" cascade;") - } +For example, if you have the following middlewares: -} +```ts title="src/api/middlewares.ts" +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/:id", + middlewares: [/* ... */], + }, + { + matcher: "/custom", + middlewares: [/* ... */], + }, + { + matcher: "/custom*", + method: ["GET"], + middlewares: [/* ... */], + }, + { + matcher: "/custom/:id", + method: ["GET"], + middlewares: [/* ... */], + }, + ], +}) ``` -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`. +The global middlewares are sorted into the following order before they're registered: -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. +1. Global middleware `/custom`. +2. Global middleware `/custom/:id`. -In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted. +And the route middlewares are sorted into the following order before they're registered: -Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. +1. Route middleware `/custom*`. +2. Route middleware `/custom/:id`. -*** +Then, the middlwares are registered in the order mentioned earlier, with global middlewares first, then the route middlewares. -## Run the Migration +### Middlewares and Route Execution Order -To run your migration, run the following command: +When a request is sent to an API route, the global middlewares are executed first, then the route middlewares, and finally the route handler. -This command also syncs module links. If you don't want that, use the `--skip-links` option. +For example, consider you have the following middlewares: + +```ts title="src/api/middlewares.ts" +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom", + middlewares: [ + (req, res, next) => { + console.log("Global middleware") + next() + }, + ], + }, + { + matcher: "/custom", + method: ["GET"], + middlewares: [ + (req, res, next) => { + console.log("Route middleware") + next() + }, + ], + }, + ], +}) +``` + +When you send a request to `/custom` route, the following messages are logged in the terminal: ```bash -npx medusa db:migrate +Global middleware +Route middleware +Hello from custom! # message logged from API route handler ``` -This reflects the changes in the database as implemented in the migration's `up` method. +The global middleware runs first, then the route middleware, and finally the route handler, assuming that it logs the message `Hello from custom!`. *** -## Rollback the Migration +## Overriding Middlewares -To rollback or revert the last migration you ran for a module, run the following command: +A middleware can not override an existing middleware. Instead, middlewares are added to the end of the middleware stack. -```bash -npx medusa db:rollback blog -``` +For example, if you define a custom validation middleware, such as `validateAndTransformBody`, on an existing route, then both the original and the custom validation middleware will run. -This rolls back the last ran migration on the Blog Module. -### Caution: Rollback Migration before Deleting +# Configure Request Body Parser -If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations. +In this chapter, you'll learn how to configure the request body parser for your API routes. -For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors. +## Default Body Parser Configuration -So, always rollback the migration before deleting it. +The Medusa application configures the body parser by default to parse JSON, URL-encoded, and text request content types. You can parse other data types by adding the relevant [Express middleware](https://expressjs.com/en/guide/using-middleware.html) or preserve the raw body data by configuring the body parser, which is useful for webhook requests. + +This chapter shares some examples of configuring the body parser for different data types or use cases. *** -## More Database Commands +## Preserve Raw Body Data for Webhooks -To learn more about the Medusa CLI's database commands, refer to [this CLI reference](https://docs.medusajs.com/resources/medusa-cli/commands/db/index.html.md). +If your API route receives webhook requests, you might want to preserve the raw body data. To do this, you can configure the body parser to parse the raw body data and store it in the `req.rawBody` property. +To do that, create the file `src/api/middlewares.ts` with the following content: -# Create a Plugin +```ts title="src/api/middlewares.ts" highlights={preserveHighlights} +import { defineMiddlewares } from "@medusajs/framework/http" -In this chapter, you'll learn how to create a Medusa plugin and publish it. +export default defineMiddlewares({ + routes: [ + { + method: ["POST"], + bodyParser: { preserveRawBody: true }, + matcher: "/custom", + }, + ], +}) +``` -A [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) is a package of reusable Medusa customizations that you can install in any Medusa application. By creating and publishing a plugin, you can reuse your Medusa customizations across multiple projects or share them with the community. +The middleware route object passed to `routes` accepts a `bodyParser` property whose value is an object of configuration for the default body parser. By enabling the `preserveRawBody` property, the raw body data is preserved and stored in the `req.rawBody` property. -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). +Learn more about [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). -## 1. Create a Plugin Project +You can then access the raw body data in your API route handler: -Plugins are created in a separate Medusa project. This makes the development and publishing of the plugin easier. Later, you'll install that plugin in your Medusa application to test it out and use it. +```ts title="src/api/custom/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -Medusa's `create-medusa-app` CLI tool provides the option to create a plugin project. Run the following command to create a new plugin project: +export async function POST( + req: MedusaRequest, + res: MedusaResponse +) { + console.log(req.rawBody) -```bash -npx create-medusa-app my-plugin --plugin + // TODO use raw body +} ``` -This will create a new Medusa plugin project in the `my-plugin` directory. +*** -### Plugin Directory Structure +## Configure Request Body Size Limit -After the installation is done, the plugin structure will look like this: +By default, the body parser limits the request body size to `100kb`. If a request body exceeds that size, the Medusa application throws an error. -![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) +You can configure the body parser to accept larger request bodies by setting the `sizeLimit` property of the `bodyParser` object in a middleware route object. For example: -- `src/`: Contains the Medusa customizations. -- `src/admin`: Contains [admin extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). -- `src/api`: Contains [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) and [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). You can add store, admin, or any custom API routes. -- `src/jobs`: Contains [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). -- `src/links`: Contains [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). -- `src/modules`: Contains [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -- `src/provider`: Contains [module providers](#create-module-providers). -- `src/subscribers`: Contains [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). -- `src/workflows`: Contains [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). You can also add [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) under `src/workflows/hooks`. -- `package.json`: Contains the plugin's package information, including general information and dependencies. -- `tsconfig.json`: Contains the TypeScript configuration for the plugin. +```ts title="src/api/middlewares.ts" highlights={sizeLimitHighlights} +import { defineMiddlewares } from "@medusajs/framework/http" -*** +export default defineMiddlewares({ + routes: [ + { + method: ["POST"], + bodyParser: { sizeLimit: "2mb" }, + matcher: "/custom", + }, + ], +}) +``` -## 2. Prepare Plugin +The `sizeLimit` property accepts one of the following types of values: -### Package Name +- A string representing the size limit in bytes (For example, `100kb`, `2mb`, `5gb`). It is passed to the [bytes](https://www.npmjs.com/package/bytes) library to parse the size. +- A number representing the size limit in bytes. For example, `1024` for 1kb. -Before developing, testing, and publishing your plugin, make sure its name in `package.json` is correct. This is the name you'll use to install the plugin in your Medusa application. +*** -For example: +## Configure File Uploads -```json title="package.json" -{ - "name": "@myorg/plugin-name", - // ... -} -``` +To accept file uploads in your API routes, you can configure the [Express Multer middleware](https://expressjs.com/en/resources/middleware/multer.html) on your route. -### Package Keywords +The `multer` package is available through the `@medusajs/medusa` package, so you don't need to install it. However, for better typing support, install the `@types/multer` package as a development dependency: -In addition, make sure that the `keywords` field in `package.json` includes the keyword `medusa-plugin` and `medusa-v2`. This helps Medusa list community plugins on the Medusa website: +```bash npm2yarn +npm install --save-dev @types/multer +``` -```json title="package.json" -{ - "keywords": [ - "medusa-plugin", - "medusa-v2" +Then, to configure file upload for your route, create the file `src/api/middlewares.ts` with the following content: + +```ts title="src/api/middlewares.ts" highlights={uploadHighlights} +import { defineMiddlewares } from "@medusajs/framework/http" +import multer from "multer" + +const upload = multer({ storage: multer.memoryStorage() }) + +export default defineMiddlewares({ + routes: [ + { + method: ["POST"], + matcher: "/custom", + middlewares: [ + // @ts-ignore + upload.array("files"), + ], + }, ], - // ... -} +}) ``` -### Package Dependencies +In the example above, you configure the `multer` middleware to store the uploaded files in memory. Then, you apply the `upload.array("files")` middleware to the route to accept file uploads. By using the `array` method, you accept multiple file uploads with the same `files` field name. -Your plugin project will already have the dependencies mentioned in this section. If you haven't made any changes to the dependencies, you can skip this section. +You can then access the uploaded files in your API route handler: -In the `package.json` file you must have the Medusa dependencies as `devDependencies` and `peerDependencies`. In addition, you must have `@swc/core` as a `devDependency`, as it's used by the plugin CLI tools. +```ts title="src/api/custom/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -For example, assuming `2.5.0` is the latest Medusa version: +export async function POST( + req: MedusaRequest, + res: MedusaResponse +) { + const files = req.files as Express.Multer.File[] -```json title="package.json" -{ - "devDependencies": { - "@medusajs/admin-sdk": "2.5.0", - "@medusajs/cli": "2.5.0", - "@medusajs/framework": "2.5.0", - "@medusajs/medusa": "2.5.0", - "@medusajs/test-utils": "2.5.0", - "@medusajs/ui": "4.0.4", - "@medusajs/icons": "2.5.0", - "@swc/core": "1.5.7", - }, - "peerDependencies": { - "@medusajs/admin-sdk": "2.5.0", - "@medusajs/cli": "2.5.0", - "@medusajs/framework": "2.5.0", - "@medusajs/test-utils": "2.5.0", - "@medusajs/medusa": "2.5.0", - "@medusajs/ui": "4.0.3", - "@medusajs/icons": "2.5.0", - } + // TODO handle files } ``` -*** - -## 3. Publish Plugin Locally for Development and Testing +The uploaded files are stored in the `req.files` property as an array of Multer file objects that have properties like `filename` and `mimetype`. -Medusa's CLI tool provides commands to simplify developing and testing your plugin in a local Medusa application. You start by publishing your plugin in the local package registry, then install it in your Medusa application. You can then watch for changes in the plugin as you develop it. +### Uploading Files using File Module Provider -### Publish and Install Local Package +The recommended way to upload the files to storage using the configured [File Module Provider](https://docs.medusajs.com/resources/architectural-modules/file/index.html.md) is to use the [uploadFilesWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md): -### Prerequisites +```ts title="src/api/custom/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { MedusaError } from "@medusajs/framework/utils" +import { uploadFilesWorkflow } from "@medusajs/medusa/core-flows" -- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) +export async function POST( + req: MedusaRequest, + res: MedusaResponse +) { + const files = req.files as Express.Multer.File[] -The first time you create your plugin, you need to publish the package into a local package registry, then install it in your Medusa application. This is a one-time only process. + if (!files?.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "No files were uploaded" + ) + } -To publish the plugin to the local registry, run the following command in your plugin project: + const { result } = await uploadFilesWorkflow(req.scope).run({ + input: { + files: files?.map((f) => ({ + filename: f.originalname, + mimeType: f.mimetype, + content: f.buffer.toString("binary"), + access: "public", + })), + }, + }) -```bash title="Plugin project" -npx medusa plugin:publish + res.status(200).json({ files: result }) +} ``` -This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. - -Next, navigate to your Medusa application: - -```bash title="Medusa application" -cd ~/path/to/medusa-app -``` +Check out the [uploadFilesWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md) for details on the expected input and output of the workflow. -Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. -Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: +# API Route Response -```bash npm2yarn title="Medusa application" -npm install --save-dev yalc -``` +In this chapter, you'll learn how to send a response in your API route. -After that, run the following Medusa CLI command to install the plugin: +## Send a JSON Response -```bash title="Medusa application" -npx medusa plugin:add @myorg/plugin-name -``` +To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler. -Make sure to replace `@myorg/plugin-name` with the name of your plugin as specified in `package.json`. Your plugin will be installed from the local package registry into your Medusa application. +For example: -### Register Plugin in Medusa Application +```ts title="src/api/custom/route.ts" highlights={jsonHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "Hello, World!", + }) +} +``` -Add the plugin to the `plugins` array in the `medusa-config.ts` file: +This API route returns the following JSON object: -```ts title="medusa-config.ts" highlights={pluginHighlights} -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "@myorg/plugin-name", - options: {}, - }, - ], -}) +```json +{ + "message": "Hello, World!" +} ``` -The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package. +*** -#### Pass Module Options through Plugin +## Set Response Status Code -Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. +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: -```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "@myorg/plugin-name", - options: { - apiKey: true, - }, - }, - ], -}) -``` +```ts title="src/api/custom/route.ts" highlights={statusHighlight} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -The `options` property in the plugin configuration is passed to all modules in the plugin. Learn more about module options in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.status(201).json({ + message: "Hello, World!", + }) +} +``` -### Watch Plugin Changes During Development +The response of this API route has the status code `201`. -While developing your plugin, you can watch for changes in the plugin and automatically update the plugin in the Medusa application using it. This is the only command you'll continuously need during your plugin development. +*** -To do that, run the following command in your plugin project: +## Change Response Content Type -```bash title="Plugin project" -npx medusa plugin:develop -``` +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. -This command will: +For example, to create an API route that returns an event stream: -- Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built. -- Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions. +```ts highlights={streamHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -### Start Medusa Application +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }) -You can start your Medusa application's development server to test out your plugin: + const interval = setInterval(() => { + res.write("Streaming data...\n") + }, 3000) -```bash npm2yarn title="Medusa application" -npm run dev + req.on("end", () => { + clearInterval(interval) + res.end() + }) +} ``` -While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. +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. *** -## 4. Create Customizations in the Plugin +## Do More with Responses -You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. +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. -- [Create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) -- [Create a module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) -- [Create a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) -- [Add a workflow hook](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) -- [Create an API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) -- [Add a subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) -- [Add a scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) -- [Add an admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) -- [Add an admin UI route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) -While building those customizations, you can test them in your Medusa application by [watching the plugin changes](#watch-plugin-changes-during-development) and [starting the Medusa application](#start-medusa-application). +# API Route Parameters -### Generating Migrations for Modules +In this chapter, you’ll learn about path, query, and request body parameters. -During your development, you may need to generate migrations for modules in your plugin. To do that, use the `plugin:db:generate` command: +## Path Parameters -```bash title="Plugin project" -npx medusa plugin:db:generate -``` +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]`. -This command generates migrations for all modules in the plugin. You can then run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command: +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: -```bash title="Medusa application" -npx medusa db:migrate -``` - -### Importing Module Resources - -Your plugin project should have the following exports in `package.json`: +```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -```json title="package.json" -{ - "exports": { - "./package.json": "./package.json", - "./workflows": "./.medusa/server/src/workflows/index.js", - "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js", - "./providers/*": "./.medusa/server/src/providers/*/index.js", - "./*": "./.medusa/server/src/*.js" - } +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[GET] Hello ${req.params.id}!`, + }) } ``` -Aside from the `./package.json` and `./providers`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export. +The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs. -The plugin exports the following files and directories: +### Multiple Path Parameters -- `./package.json`: The package.json file. Medusa needs to access the `package.json` when registering the plugin. -- `./workflows`: The workflows exported in `./src/workflows/index.ts`. -- `./.medusa/server/src/modules/*`: The definition file of modules. This is useful if you create links to the plugin's modules in the Medusa application. -- `./providers/*`: The definition file of module providers. This allows you to register the plugin's providers in the Medusa application. -- `./*`: Any other files in the plugin's `src` directory. +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]`. -With these exports, you can import the plugin's resources in the Medusa application's code like this: +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: -`@myorg/plugin-name` is the plugin package's name. +```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -```ts -import { Workflow1, Workflow2 } from "@myorg/plugin-name/workflows" -import BlogModule from "@myorg/plugin-name/modules/blog" -// import other files created in plugin like ./src/types/blog.ts -import BlogType from "@myorg/plugin-name/types/blog" +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[GET] Hello ${ + req.params.id + } - ${req.params.name}!`, + }) +} ``` -And you can register a module provider in the Medusa application's `medusa-config.ts` like this: +You access the `id` and `name` path parameters using the `req.params` property. -```ts highlights={[["9"]]} title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/notification", - options: { - providers: [ - { - resolve: "@myorg/plugin-name/providers/my-notification", - id: "my-notification", - options: { - channels: ["email"], - // provider options... - }, - }, - ], - }, - }, - ], -}) -``` +*** -You pass to `resolve` the path to the provider relative to the plugin package. So, in this example, the `my-notification` provider is located in `./src/providers/my-notification/index.ts` of the plugin. +## Query Parameters -### Create Module Providers +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. -To learn how to create module providers, refer to the following guides: +For example: -- [File Module Provider](https://docs.medusajs.com/resources/references/file-provider-module/index.html.md) -- [Notification Module Provider](https://docs.medusajs.com/resources/references/notification-provider-module/index.html.md) -- [Auth Module Provider](https://docs.medusajs.com/resources/references/auth/provider/index.html.md) -- [Payment Module Provider](https://docs.medusajs.com/resources/references/payment/provider/index.html.md) -- [Fulfillment Module Provider](https://docs.medusajs.com/resources/references/fulfillment/provider/index.html.md) -- [Tax Module Provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md) +```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}`, + }) +} +``` -## 5. Publish Plugin to NPM +The value of `req.query.name` is the value passed in `?name=John`, for example. -Medusa's CLI tool provides a command that bundles your plugin to be published to npm. Once you're ready to publish your plugin publicly, run the following command in your plugin project: +### Validate Query Parameters -```bash -npx medusa plugin:build -``` +You can apply validation rules on received query parameters to ensure they match specified rules and types. -The command will compile an output in the `.medusa/server` directory. +Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-query-paramters/index.html.md). -You can now publish the plugin to npm using the [NPM CLI tool](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Run the following command to publish the plugin to npm: +*** -```bash -npm publish -``` +## Request Body Parameters -If you haven't logged in before with your NPM account, you'll be asked to log in first. Then, your package is published publicly to be used in any Medusa application. +The Medusa application parses the body of any request having a JSON, URL-encoded, or text request content types. The request body parameters are set in the `MedusaRequest`'s `body` property. -### Install Public Plugin in Medusa Application +Learn more about configuring body parsing in [this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md). -You install a plugin that's published publicly using your package manager. For example: +For example: -```bash npm2yarn -npm install @myorg/plugin-name -``` +```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -Where `@myorg/plugin-name` is the name of your plugin as published on NPM. +type HelloWorldReq = { + name: string +} -Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). +export const POST = async ( + req: MedusaRequest, + 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. -## Update a Published Plugin +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. -To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md). +To test it out, send the following request to your Medusa application: -If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. +```bash +curl -X POST 'http://localhost:9000/hello-world' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "name": "John" +}' +``` -First, run the following command to change the version of the plugin: +This returns the following JSON object: -```bash -npm version +```json +{ + "message": "[POST] Hello John!" +} ``` -Where `` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. Refer to the [npm version documentation](https://docs.npmjs.com/cli/v10/commands/npm-version) for more information. +### Validate Body Parameters -Then, re-run the same commands for publishing a plugin: +You can apply validation rules on received body parameters to ensure they match specified rules and types. -```bash -npx medusa plugin:build -npm publish -``` +Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md). -This will publish an updated version of your plugin under a new version. +# Protected Routes -# Add Columns to a Link Table +In this chapter, you’ll learn how to create protected routes. -In this chapter, you'll learn how to add custom columns to a link definition's table and manage them. +## What is a Protected Route? -## Link Table's Default Columns +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. -When you define a link between two data models, Medusa creates a link table in the database to store the IDs of the linked records. You can learn more about the created table in the [Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). +*** -In various cases, you might need to store additional data in the link table. For example, if you define a link between a `product` and a `post`, you might want to store the publish date of the product's post in the link table. +## Default Protected Routes -In those cases, you can add a custom column to a link's table in the link definition. You can later set that column whenever you create or update a link between the linked records. +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. + +Refer to the API Reference for [Admin](https://docs.medusajs.com/api/admin#authentication) and [Store](https://docs.medusajs.com/api/store#authentication) authentication methods. *** -## How to Add Custom Columns to a Link's Table? +## Protect Custom API Routes -The `defineLink` function used to define a link accepts a third parameter, which is an object of options. +To protect custom API Routes to only allow authenticated customer or admin users, use the `authenticate` middleware from the Medusa Framework. -To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property: +For example: -```ts highlights={linkHighlights} -import BlogModule from "../modules/blog" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" +```ts title="src/api/middlewares.ts" highlights={highlights} +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" -export default defineLink( - ProductModule.linkable.product, - BlogModule.linkable.blog, - { - database: { - extraColumns: { - metadata: { - type: "json", - }, - }, +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/admin*", + middlewares: [authenticate("user", ["session", "bearer", "api-key"])], }, - } -) + { + matcher: "/custom/customer*", + middlewares: [authenticate("customer", ["session", "bearer"])], + }, + ], +}) ``` -This adds to the table created for the link between `product` and `blog` a `metadata` column of type `json`. +The `authenticate` middleware function accepts three parameters: -### Database Options +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 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 properties: + - `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. + - `allowUnregistered` (default: `false`): A boolean indicating if unregistered users should be allowed access. This is useful when you want to allow users who aren’t registered to access certain routes. -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. +## Authentication Opt-Out -`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 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. +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: -Learn more about Link, how to resolve it, and its methods in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md). - -```ts -await link.create({ - [Modules.PRODUCT]: { - product_id: "123", - }, - [BLOG_MODULE]: { - post_id: "321", - }, - data: { - metadata: { - test: true, - }, - }, -}) -``` - -*** - -## Retrieve Custom Column with Link - -To retrieve linked records with their custom columns, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). A module link's definition, exported by a file under `src/links`, has a special `entryPoint` property. Use this property when specifying the `entity` property in Query's `graph` method. +```ts title="src/api/admin/custom/route.ts" highlights={[["15"]]} +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -For example: +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "Hello", + }) +} -```ts highlights={retrieveHighlights} -import productPostLink from "../links/product-post" +export const AUTHENTICATE = false +``` -// ... +Now, any request sent to the `/admin/custom` API route is allowed, regardless if the admin user is authenticated. -const { data } = await query.graph({ - entity: productPostLink.entryPoint, - fields: ["metadata", "product.*", "post.*"], - filters: { - product_id: "prod_123", - }, -}) -``` +*** -This retrieves the product of id `prod_123` and its linked `post` records. +## Authenticated Request Type -In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link. +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`. -## Update Custom Column's Value +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. -Link's `create` method updates a link's data if the link between the specified records already exists. +### Retrieve Logged-In Customer's Details -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. +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 -await link.create({ - [Modules.PRODUCT]: { - product_id: "123", - }, - [BLOG_MODULE]: { - post_id: "321", - }, - data: { - metadata: { - test: false, - }, - }, -}) -``` +```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 + ) -# Link + const customer = await customerModuleService.retrieveCustomer( + req.auth_context.actor_id + ) + } -In this chapter, you’ll learn what Link is and how to use it to manage links. + // ... +} +``` -As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), Remote Link has been deprecated in favor of Link. They have the same usage, so you only need to change the key used to resolve the tool from the Medusa container as explained below. +In this example, you resolve the Customer Module's main service, then use it to retrieve the logged-in customer, if available. -## What is Link? +### Retrieve Logged-In Admin User's Details -Link is a class with utility methods to manage links between data models. It’s registered in the Medusa container under the `link` registration name. +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 collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - MedusaRequest, +```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 { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +import { Modules } from "@medusajs/framework/utils" +import { IUserModuleService } from "@medusajs/framework/types" -export async function POST( - req: MedusaRequest, +export const GET = async ( + req: AuthenticatedMedusaRequest, res: MedusaResponse -): Promise { - const link = req.scope.resolve( - ContainerRegistrationKeys.LINK +) => { + const userModuleService: IUserModuleService = req.scope.resolve( + Modules.USER ) - + + const user = await userModuleService.retrieveUser( + req.auth_context.actor_id + ) + // ... } ``` -You can use its methods to manage links, such as create or delete links. - -*** - -## Create Link +In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user. -To create a link between records of two data models, use the `create` method of Link. -For example: +# Request Body and Query Parameter Validation -```ts -import { Modules } from "@medusajs/framework/utils" +In this chapter, you'll learn how to validate request body and query parameters in your custom API route. -// ... +## Request Validation -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_123", - }, -}) -``` +Consider you're creating a `POST` API route at `/custom`. It accepts two parameters `a` and `b` that are required numbers, and returns their sum. -The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules. +Medusa provides two middlewares to validate the request body and query paramters of incoming requests to your custom API routes: -The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition. +- `validateAndTransformBody` to validate the request's body parameters against a schema. +- `validateAndTransformQuery` to validate the request's query parameters against a schema. -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. +Both middlewares accept a [Zod](https://zod.dev/) schema as a parameter, which gives you flexibility in how you define your validation schema with complex rules. -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. +The next steps explain how to add request body and query parameter validation to the API route mentioned earlier. *** -## Dismiss Link +## How to Validate Request Body -To remove a link between records of two data models, use the `dismiss` method of Link. +### Step 1: Create Validation Schema -For example: +Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. -```ts -import { Modules } from "@medusajs/framework/utils" +To create a validation schema with Zod, 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: -await link.dismiss({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_123", - }, +```ts title="src/api/custom/validators.ts" +import { z } from "zod" + +export const PostStoreCustomSchema = z.object({ + a: z.number(), + b: z.number(), }) ``` -The `dismiss` method accepts the same parameter type as the [create method](#create-link). - -The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition. - -*** +The `PostStoreCustomSchema` variable is a Zod schema that indicates the request body is valid if: -## Cascade Delete Linked Records +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. -If a record is deleted, use the `delete` method of Link to delete all linked records. +### Step 2: Add Request Body Validation Middleware -For example: +To use this schema for validating the body parameters of requests to `/custom`, use the `validateAndTransformBody` middleware provided by `@medusajs/framework/http`. It accepts the Zod schema as a parameter. -```ts -import { Modules } from "@medusajs/framework/utils" +For example, create the file `src/api/middlewares.ts` with the following content: -// ... +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformBody, +} from "@medusajs/framework/http" +import { PostStoreCustomSchema } from "./custom/validators" -await productModuleService.deleteVariants([variant.id]) - -await link.delete({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom", + method: "POST", + middlewares: [ + validateAndTransformBody(PostStoreCustomSchema), + ], + }, + ], }) ``` -This deletes all records linked to the deleted product. +This applies the `validateAndTransformBody` middleware on `POST` requests to `/custom`. It uses the `PostStoreCustomSchema` as the validation schema. -*** +#### How the Validation Works -## Restore Linked Records +If a request's body parameters don't pass the validation, the `validateAndTransformBody` middleware throws an error indicating the validation errors. -If a record that was previously soft-deleted is now restored, use the `restore` method of Link to restore all linked records. +If a request's body parameters are validated successfully, the middleware sets the validated body parameters in the `validatedBody` property of `MedusaRequest`. -For example: +### Step 3: Use Validated Body in API Route -```ts -import { Modules } from "@medusajs/framework/utils" +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: -await productModuleService.restoreProducts(["prod_123"]) +```ts title="src/api/custom/route.ts" highlights={routeHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { z } from "zod" +import { PostStoreCustomSchema } from "./validators" -await link.restore({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, -}) -``` +type PostStoreCustomSchemaType = z.infer< + typeof PostStoreCustomSchema +> +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + sum: req.validatedBody.a + req.validatedBody.b, + }) +} +``` -# Module Link Direction +In the API route, you use the `validatedBody` property of `MedusaRequest` to access the values of the `a` and `b` properties. -In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case. +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. -## Link Direction +### Test it Out -The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`. +To test out the validation, send a `POST` request to `/custom` passing `a` and `b` body parameters. You can try sending incorrect request body parameters to test out the validation. -For example, the following defines a link from the `helloModuleService`'s `myCustom` data model to the Product Module's `product` data model: +For example, if you omit the `a` parameter, you'll receive a `400` response code with the following response data: -```ts -export default defineLink( - HelloModule.linkable.myCustom, - ProductModule.linkable.product -) +```json +{ + "type": "invalid_data", + "message": "Invalid request: Field 'a' is required" +} ``` -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 -) -``` +## How to Validate Request Query Paramters -The above links are two different links that serve different purposes. +The steps to validate the request query parameters are the similar to that of [validating the body](#how-to-validate-request-body). -*** +### Step 1: Create Validation Schema -## Which Link Direction to Use? +The first step is to create a schema with Zod with the rules of the accepted query parameters. -### Extend Data Models +Consider that the API route accepts two query parameters `a` and `b` that are numbers, similar to the previous section. -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. +Create the file `src/api/custom/validators.ts` with the following content: -For example, consider you want to add a `subtitle` custom field to the `product` data model. To do that, you define a `Subtitle` data model in your module, then define a link from the `Product` data model to it: +```ts title="src/api/custom/validators.ts" +import { z } from "zod" -```ts -export default defineLink( - ProductModule.linkable.product, - HelloModule.linkable.subtitle -) +export const PostStoreCustomSchema = z.object({ + a: z.preprocess( + (val) => { + if (val && typeof val === "string") { + return parseInt(val) + } + return val + }, + z + .number() + ), + b: z.preprocess( + (val) => { + if (val && typeof val === "string") { + return parseInt(val) + } + return val + }, + z + .number() + ), +}) ``` -### Associate Data Models +Since a query parameter's type is originally a string or array of strings, you have to use Zod's `preprocess` method to validate other query types, such as numbers. -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 both `a` and `b`, you transform the query parameter's value to an integer first if it's a string, then, you check that the resulting value is a number. -For example, consider you have `Post` data model representing a blog post, and you want to associate a blog post with a product. To do that, define a link from the `Post` data model to `Product`: +### Step 2: Add Request Query Validation Middleware -```ts -export default defineLink( - HelloModule.linkable.post, - ProductModule.linkable.product -) -``` +Next, you'll use the schema to validate incoming requests' query parameters to the `/custom` API route. +Add the `validateAndTransformQuery` middleware to the API route in the file `src/api/middlewares.ts`: -# Query +```ts title="src/api/middlewares.ts" +import { + validateAndTransformQuery, + defineMiddlewares, +} from "@medusajs/framework/http" +import { PostStoreCustomSchema } from "./custom/validators" -In this chapter, you’ll learn about Query and how to use it to fetch data from modules. +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom", + method: "POST", + middlewares: [ + validateAndTransformQuery( + PostStoreCustomSchema, + {} + ), + ], + }, + ], +}) +``` -## What is Query? +The `validateAndTransformQuery` accepts two parameters: -Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key. +- The first one is the Zod schema to validate the query parameters against. +- The second one is an object of options for retrieving data using Query, which you can learn more about in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). -In all resources that can access the [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s commerce modules. +#### How the Validation Works -*** +If a request's query parameters don't pass the validation, the `validateAndTransformQuery` middleware throws an error indicating the validation errors. -## Query Example +If a request's query parameters are validated successfully, the middleware sets the validated query parameters in the `validatedQuery` property of `MedusaRequest`. -For example, create the route `src/api/query/route.ts` with the following content: +### Step 3: Use Validated Query in API Route -```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" +Finally, use the validated query in the API route. The `MedusaRequest` parameter has a `validatedQuery` parameter that you can use to access the validated parameters. + +For example, create the file `src/api/custom/route.ts` with the following content: + +```ts title="src/api/custom/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" export const GET = async ( req: MedusaRequest, res: MedusaResponse ) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + const a = req.validatedQuery.a as number + const b = req.validatedQuery.b as number - const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], + res.json({ + sum: a + b, }) - - res.json({ my_customs: myCustoms }) } ``` -In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key. +In the API route, you use the `validatedQuery` property of `MedusaRequest` to access the values of the `a` and `b` properties as numbers, then return in the response their sum. -Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties: +### Test it Out -- `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. +To test out the validation, send a `POST` request to `/custom` with `a` and `b` query parameters. You can try sending incorrect query parameters to see how the validation works. -The method returns an object that has a `data` property, which holds an array of the retrieved data. For example: +For example, if you omit the `a` parameter, you'll receive a `400` response code with the following response data: -```json title="Returned Data" +```json { - "data": [ - { - "id": "123", - "name": "test" - } - ] + "type": "invalid_data", + "message": "Invalid request: Field 'a' is required" } ``` *** -## Querying the Graph - -When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates. +## Learn More About Validation Schemas -This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them. +To see different examples and learn more about creating a validation schema, refer to [Zod's documentation](https://zod.dev). -*** -## Retrieve Linked Records +# Event Data Payload -Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`. +In this chapter, you'll learn how subscribers receive an event's data payload. -For example: +## Access Event's Data Payload -```ts highlights={[["6"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: [ - "id", - "name", - "product.*", - ], -}) -``` +When events are emitted, they’re emitted with a data payload. -`.*` 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`. +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. -### Retrieve List Link Records +For example: -If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`. +```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" -For example: +export default async function productCreateHandler({ + event, +}: SubscriberArgs<{ id: string }>) { + const productId = event.data.id + console.log(`The product ${productId} was created`) +} -```ts highlights={[["6"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: [ - "id", - "name", - "products.*", - ], -}) +export const config: SubscriberConfig = { + event: "product.created", +} ``` -### Apply Filters and Pagination on Linked Records - -Consider you want to apply filters or pagination configurations on the product(s) linked to `my_custom`. To do that, you must query the module link's table instead. +The `event` object has the following properties: -As mentioned in the [Module Link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) documentation, Medusa creates a table for your module link. So, not only can you retrieve linked records, but you can also retrieve the records in a module link's table. +- data: (\`object\`) The data payload of the event. Its properties are different for each event. +- name: (string) The name of the triggered event. +- metadata: (\`object\`) Additional data and context of the emitted event. -A module link's definition, exported by a file under `src/links`, has a special `entryPoint` property. Use this property when specifying the `entity` property in Query's `graph` method. +This logs the product ID received in the `product.created` event’s data payload to the console. -For example: +{/* --- -```ts highlights={queryLinkTableHighlights} -import productCustomLink from "../../../links/product-custom" +## 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. */} -const { data: productCustoms } = await query.graph({ - entity: productCustomLink.entryPoint, - fields: ["*", "product.*", "my_custom.*"], - pagination: { - take: 5, - skip: 0, - }, -}) -``` -In the object passed to the `graph` method: +# Add Data Model Check Constraints -- You pass the `entryPoint` property of the link definition as the value for `entity`. So, Query will retrieve records from the module link's table. -- You pass three items to the `field` property: - - `*` to retrieve the link table's fields. This is useful if the link table has [custom columns](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns/index.html.md). - - `product.*` to retrieve the fields of a product record linked to a `MyCustom` record. - - `my_custom.*` to retrieve the fields of a `MyCustom` record linked to a product record. +In this chapter, you'll learn how to add check constraints to your data model. -You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination). +## What is a Check Constraint? -The returned `data` is similar to the following: +A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown. -```json title="Example Result" -[{ - "id": "123", - "product_id": "prod_123", - "my_custom_id": "123", - "product": { - "id": "prod_123", - // other product fields... - }, - "my_custom": { - "id": "123", - // other my_custom fields... - } -}] -``` +For example, if you have a data model with a `price` property, you want to only allow positive number values. So, you add a check constraint that fails when inserting a record with a negative price value. *** -## Apply Filters +## How to Set a Check Constraint? -```ts highlights={[["6"], ["7"], ["8"], ["9"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], - filters: { - id: [ - "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX", - "mc_01HWSVWK3KYHKQEE6QGS2JC3FX", - ], - }, +To set check constraints on a data model, use the `checks` method. This method accepts an array of check constraints to apply on the data model. + +For example, to set a check constraint on a `price` property that ensures its value can only be a positive number: + +```ts highlights={checks1Highlights} +import { model } from "@medusajs/framework/utils" + +const CustomProduct = model.define("custom_product", { + // ... + price: model.bigNumber(), }) +.checks([ + (columns) => `${columns.price} >= 0`, +]) ``` -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. +The item passed in the array parameter of `checks` can be a callback function that accepts as a parameter an object whose keys are the names of the properties in the data model schema, and values the respective column name in the database. -Filters don't apply on fields of linked data models from other modules. +The function returns a string indicating the [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). In the expression, use the `columns` parameter to access a property's column name. -*** +You can also pass an object to the `checks` method: -## Apply Pagination +```ts highlights={checks2Highlights} +import { model } from "@medusajs/framework/utils" -```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, - }, +const CustomProduct = model.define("custom_product", { + // ... + price: model.bigNumber(), }) +.checks([ + { + name: "custom_product_price_check", + expression: (columns) => `${columns.price} >= 0`, + }, +]) ``` -The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records. +The object accepts the following properties: -To paginate the returned records, pass the following properties to `pagination`: +- `name`: The check constraint's name. +- `expression`: A function similar to the one that can be passed to the array. It accepts an object of columns and returns an [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). -- `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: +## Apply in Migrations -- skip: (\`number\`) The number of records skipped. -- take: (\`number\`) The number of records requested to fetch. -- count: (\`number\`) The total number of records. +After adding the check constraint, make sure to generate and run migrations if you already have the table in the database. Otherwise, the check constraint won't be reflected. -### Sort Records +To generate a migration for the data model's module then reflect it on the database, run the following command: -```ts highlights={[["5"], ["6"], ["7"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], - pagination: { - order: { - name: "DESC", - }, - }, -}) +```bash +npx medusa db:generate custom_module +npx medusa db:migrate ``` -Sorting doesn't work on fields of linked data models from other modules. +The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. -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: +# Configure Data Model Properties -- `ASC` to sort records by that property in ascending order. -- `DESC` to sort records by that property in descending order. +In this chapter, you’ll learn how to configure data model properties. -*** +## Property’s Default Value -## Request Query Configurations +Use the `default` method on a property's definition to specify the default value of a property. -For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that: +For example: -- Validates accepted query parameters, as explained in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). -- Parses configurations that are received as query parameters to be passed to Query. +```ts highlights={defaultHighlights} +import { model } from "@medusajs/framework/utils" -Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request. +const MyCustom = model.define("my_custom", { + color: model + .enum(["black", "white"]) + .default("black"), + age: model + .number() + .default(0), + // ... +}) -### Step 1: Add Middleware +export default MyCustom +``` -The first step is to use the `validateAndTransformQuery` middleware on the `GET` route. You add the middleware in `src/api/middlewares.ts`: +In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`. -```ts title="src/api/middlewares.ts" -import { - validateAndTransformQuery, - defineMiddlewares, -} from "@medusajs/framework/http" -import { createFindParams } from "@medusajs/medusa/api/utils/validators" +*** -export const GetCustomSchema = createFindParams() +## Nullable Property -export default defineMiddlewares({ - routes: [ - { - matcher: "/customs", - method: "GET", - middlewares: [ - validateAndTransformQuery( - GetCustomSchema, - { - defaults: [ - "id", - "name", - "products.*", - ], - isList: true, - } - ), - ], - }, - ], +Use the `nullable` method to indicate that a property’s value can be `null`. + +For example: + +```ts highlights={nullableHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + price: model.bigNumber().nullable(), + // ... }) + +export default MyCustom ``` -The `validateAndTransformQuery` accepts two parameters: +*** -1. A Zod validation schema for the query parameters, which you can learn more about in the [API Route Validation documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). Medusa has a `createFindParams` utility that generates a Zod schema that accepts four query parameters: - 1. `fields`: The fields and relations to retrieve in the returned resources. - 2. `offset`: The number of items to skip before retrieving the returned items. - 3. `limit`: The maximum number of items to return. - 4. `order`: The fields to order the returned items by in ascending or descending order. -2. A Query configuration object. It accepts the following properties: - 1. `defaults`: An array of default fields and relations to retrieve in each resource. - 2. `isList`: A boolean indicating whether a list of items are returned in the response. - 3. `allowed`: An array of fields and relations allowed to be passed in the `fields` query parameter. - 4. `defaultLimit`: A number indicating the default limit to use if no limit is provided. By default, it's `50`. +## Unique Property -### Step 2: Use Configurations in API Route +The `unique` method indicates that a property’s value must be unique in the database through a unique index. -After applying this middleware, your API route now accepts the `fields`, `offset`, `limit`, and `order` query parameters mentioned above. +For example: -The middleware transforms these parameters to configurations that you can pass to Query in your API route handler. These configurations are stored in the `queryConfig` parameter of the `MedusaRequest` object. +```ts highlights={uniqueHighlights} +import { model } from "@medusajs/framework/utils" -As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), `remoteQueryConfig` has been depercated in favor of `queryConfig`. Their usage is still the same, only the property name has changed. +const User = model.define("user", { + email: model.text().unique(), + // ... +}) -For example, Create the file `src/api/customs/route.ts` with the following content: +export default User +``` -```ts title="src/api/customs/route.ts" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +In this example, multiple users can’t have the same email. -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - const { data: myCustoms } = await query.graph({ - entity: "my_custom", - ...req.queryConfig, - }) +# Emit Workflow and Service Events - res.json({ my_customs: myCustoms }) -} -``` +In this chapter, you'll learn about event types and how to emit an event in a service or workflow. -This adds a `GET` API route at `/customs`, which is the API route you added the middleware for. +## Event Types -In the API route, you pass `req.queryConfig` to `query.graph`. `queryConfig` has properties like `fields` and `pagination` to configure the query based on the default values you specified in the middleware, and the query parameters passed in the request. +In your customization, you can emit an event, then listen to it in a subscriber and perform an asynchronus action, such as send a notification or data to a third-party system. -### Test it Out +There are two types of events in Medusa: -To test it out, start your Medusa application and send a `GET` request to the `/customs` API route. A list of records are retrieved with the specified fields in the middleware. +1. Workflow event: an event that's emitted in a workflow after a commerce feature is performed. For example, Medusa emits the `order.placed` event after a cart is completed. +2. Service event: an event that's emitted to track, trace, or debug processes under the hood. For example, you can emit an event with an audit trail. -```json title="Returned Data" -{ - "my_customs": [ - { - "id": "123", - "name": "test" - } - ] -} -``` +### Which Event Type to Use? -Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result. +**Workflow events** are the most common event type in development, as most custom features and customizations are built around workflows. -Learn more about [specifing fields and relations](https://docs.medusajs.com/api/store#select-fields-and-relations) and [pagination](https://docs.medusajs.com/api/store#pagination) in the API reference. +Some examples of workflow events: +1. When a user creates a blog post and you're emitting an event to send a newsletter email. +2. When you finish syncing products to a third-party system and you want to notify the admin user of new products added. +3. When a customer purchases a digital product and you want to generate and send it to them. -# Scheduled Jobs Number of Executions +You should only go for a **service event** if you're emitting an event for processes under the hood that don't directly affect front-facing features. -In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. +Some examples of service events: -## numberOfExecutions Option +1. When you're tracing data manipulation and changes, and you want to track every time some custom data is changed. +2. When you're syncing data with a search engine. -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. +*** + +## Emit Event in a Workflow + +To emit a workflow event, use the `emitEventStep` helper step provided in the `@medusajs/medusa/core-flows` package. For example: ```ts highlights={highlights} -export default async function myCustomJob() { - console.log("I'll be executed three times only.") -} +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + emitEventStep, +} from "@medusajs/medusa/core-flows" -export const config = { - name: "hello-world", - // execute every minute - schedule: "* * * * *", - numberOfExecutions: 3, -} -``` +const helloWorldWorkflow = createWorkflow( + "hello-world", + () => { + // ... -The above scheduled job has the `numberOfExecutions` configuration set to `3`. + emitEventStep({ + eventName: "custom.created", + data: { + id: "123", + // other data payload + }, + }) + } +) +``` -So, it'll only execute 3 times, each every minute, then it won't be executed anymore. +The `emitEventStep` accepts an object having the following properties: -If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. +- `eventName`: The event's name. +- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. +In this example, you emit the event `custom.created` and pass in the data payload an ID property. -# Query Context +### Test it Out -In this chapter, you'll learn how to pass contexts when retrieving data with [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). +If you execute the workflow, the event is emitted and you can see it in your application's logs. -## What is Query Context? +Any subscribers listening to the event are executed. -Query context is a way to pass additional information when retrieving data with Query. This data can be useful when applying custom transformations to the retrieved data based on the current context. +*** -For example, consider you have a Blog Module with posts and authors. You can accept the user's language as a context and return the posts in the user's language. Another example is how Medusa uses Query Context to [retrieve product variants' prices based on the customer's currency](https://docs.medusajs.com/resources/commerce-modules/product/guides/price/index.html.md). +## Emit Event in a Service -*** +To emit a service event: -## How to Use Query Context +1. Resolve `event_bus` from the module's container in your service's constructor: -The `query.graph` method accepts an optional `context` parameter that can be used to pass additional context either to the data model you're retrieving (for example, `post`), or its related and linked models (for example, `author`). +### Extending Service Factory -You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument. +```ts title="src/modules/hello/service.ts" highlights={["9"]} +import { IEventBusService } from "@medusajs/framework/types" +import { MedusaService } from "@medusajs/framework/utils" -For example, to retrieve posts using Query while passing the user's language as a context: +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + protected eventBusService_: AbstractEventBusModuleService -```ts -const { data } = await query.graph({ - entity: "post", - fields: ["*"], - context: QueryContext({ - lang: "es", - }), -}) + constructor({ event_bus }) { + super(...arguments) + this.eventBusService_ = event_bus + } +} ``` -In this example, you pass in the context a `lang` property whose value is `es`. - -Then, to handle the context while retrieving records of the data model, in the associated module's service you override the generated `list` method of the data model. +### Without Service Factory -For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context: +```ts title="src/modules/hello/service.ts" highlights={["6"]} +import { IEventBusService } from "@medusajs/framework/types" -```ts highlights={highlights2} -import { MedusaContext, MedusaService } from "@medusajs/framework/utils" -import { Context, FindConfig } from "@medusajs/framework/types" -import Post from "./models/post" -import Author from "./models/author" +class HelloModuleService { + protected eventBusService_: AbstractEventBusModuleService -class BlogModuleService extends MedusaService({ - Post, - Author, -}){ - // @ts-ignore - async listPosts( - filters?: any, - config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context + constructor({ event_bus }) { + this.eventBusService_ = event_bus + } +} +``` - let posts = await super.listPosts(filters, config, sharedContext) +2. Use the event bus service's `emit` method in the service's methods to emit an event: - if (context.lang === "es") { - posts = posts.map((post) => { - return { - ...post, - title: post.title + " en español", - } - }) - } +```ts title="src/modules/hello/service.ts" highlights={serviceHighlights} +class HelloModuleService { + // ... + performAction() { + // TODO perform action - return posts + this.eventBusService_.emit({ + name: "custom.event", + data: { + id: "123", + // other data payload + }, + }) } } - -export default BlogModuleService ``` -In the above example, you override the generated `listPosts` method. This method receives as a first parameter the filters passed to the query, but it also includes a `context` property that holds the context passed to the query. +The method accepts an object having the following properties: -You extract the context from `filters`, then retrieve the posts using the parent's `listPosts` method. After that, if the language is set in the context, you transform the titles of the posts. +- `name`: The event's name. +- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. -All posts returned will now have their titles appended with "en español". +3. By default, the Event Module's service isn't injected into your module's container. To add it to the container, pass it in the module's registration object in `medusa-config.ts` in the `dependencies` property: -Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md). +```ts title="medusa-config.ts" highlights={depsHighlight} +import { Modules } from "@medusajs/framework/utils" -### Using Pagination with Query +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/hello", + dependencies: [ + Modules.EVENT_BUS, + ], + }, + ], +}) +``` -If you pass pagination fields to `query.graph`, you must also override the `listAndCount` method in the service. +The `dependencies` property accepts an array of module registration keys. The specified modules' main services are injected into the module's container. -For example, following along with the previous example, you must override the `listAndCountPosts` method of the Blog Module's service: +That's how you can resolve it in your module's main service's constructor. -```ts -import { MedusaContext, MedusaService } from "@medusajs/framework/utils" -import { Context, FindConfig } from "@medusajs/framework/types" -import Post from "./models/post" -import Author from "./models/author" +### Test it Out -class BlogModuleService extends MedusaService({ - Post, - Author, -}){ - // @ts-ignore - async listAndCountPosts( - filters?: any, - config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context +If you execute the `performAction` method of your service, the event is emitted and you can see it in your application's logs. - const result = await super.listAndCountPosts( - filters, - config, - sharedContext - ) +Any subscribers listening to the event are also executed. - if (context.lang === "es") { - result.posts = posts.map((post) => { - return { - ...post, - title: post.title + " en español", - } - }) - } - return result - } -} +# Data Model Default Properties -export default BlogModuleService -``` - -Now, the `listAndCountPosts` method will handle the context passed to `query.graph` when you pass pagination fields. You can also move the logic to transform the posts' titles to a separate method and call it from both `listPosts` and `listAndCountPosts`. - -*** - -## Passing Query Context to Related Data Models - -If you're retrieving a data model and you want to pass context to its associated model in the same module, you can pass them as part of `QueryContext`'s parameter, then handle them in the same `list` method. +In this chapter, you'll learn about the properties available by default in your data model. -For linked data models, check out the [next section](#passing-query-context-to-linked-data-models). +When you create a data model, the following properties are created for you by Medusa: -For example, to pass a context for the post's authors: +- `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. -```ts highlights={highlights3} -const { data } = await query.graph({ - entity: "post", - fields: ["*"], - context: QueryContext({ - lang: "es", - author: QueryContext({ - lang: "es", - }), - }), -}) -``` -Then, in the `listPosts` method, you can handle the context for the post's authors: +# Data Model Database Index -```ts highlights={highlights4} -import { MedusaContext, MedusaService } from "@medusajs/framework/utils" -import { Context, FindConfig } from "@medusajs/framework/types" -import Post from "./models/post" -import Author from "./models/author" +In this chapter, you’ll learn how to define a database index on a data model. -class BlogModuleService extends MedusaService({ - Post, - Author, -}){ - // @ts-ignore - async listPosts( - filters?: any, - config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context +## Define Database Index on Property - let posts = await super.listPosts(filters, config, sharedContext) +Use the `index` method on a property's definition to define a database index. - const isPostLangEs = context.lang === "es" - const isAuthorLangEs = context.author?.lang === "es" +For example: - if (isPostLangEs || isAuthorLangEs) { - posts = posts.map((post) => { - return { - ...post, - title: isPostLangEs ? post.title + " en español" : post.title, - author: { - ...post.author, - name: isAuthorLangEs ? post.author.name + " en español" : post.author.name, - }, - } - }) - } +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" - return posts - } -} +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text().index( + "IDX_MY_CUSTOM_NAME" + ), +}) -export default BlogModuleService +export default MyCustom ``` -The context in `filters` will also have the context for `author`, which you can use to make transformations to the post's authors. +The `index` method optionally accepts the name of the index as a parameter. + +In this example, you define an index on the `name` property. *** -## Passing Query Context to Linked Data Models +## Define Database Index on Data Model -If you're retrieving a data model and you want to pass context to a linked model in a different module, pass to the `context` property an object instead, where its keys are the linked model's name and the values are the context for that linked model. +A data model has an `indexes` method that defines database indices on its properties. -For example, consider the Product Module's `Product` data model is linked to the Blog Module's `Post` data model. You can pass context to the `Post` data model while retrieving products like so: +The index can be on multiple columns (composite index). For example: -```ts highlights={highlights5} -const { data } = await query.graph({ - entity: "product", - fields: ["*", "post.*"], - context: { - post: QueryContext({ - lang: "es", - }), +```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 ``` -In this example, you retrieve products and their associated posts. You also pass a context for `post`, indicating the customer's language. +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. -To handle the context, you override the generated `listPosts` method of the Blog Module as explained [previously](#how-to-use-query-context). +In the above example, you define a composite index on the `name` and `age` properties. +### Index Conditions -# Commerce Modules +An index can have conditions. For example: -In this chapter, you'll learn about Medusa's commerce modules. +```ts highlights={conditionHighlights} +import { model } from "@medusajs/framework/utils" -## What is a Commerce Module? +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + where: { + age: 30, + }, + }, +]) -Commerce modules are built-in [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) of Medusa that provide core commerce logic specific to domains like Products, Orders, Customers, Fulfillment, and much more. +export default MyCustom +``` -Medusa's commerce modules are used to form Medusa's default [workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) and [APIs](https://docs.medusajs.com/api/store). For example, when you call the add to cart endpoint. the add to cart workflow runs which uses the Product Module to check if the product exists, the Inventory Module to ensure the product is available in the inventory, and the Cart Module to finally add the product to the cart. +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. -You'll find the details and steps of the add-to-cart workflow in [this workflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/addToCartWorkflow/index.html.md) +In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`. -The core commerce logic contained in Commerce Modules is also available directly when you are building customizations. This granular access to commerce functionality is unique and expands what's possible to build with Medusa drastically. +A property's condition can be a negation. For example: -### List of Medusa's Commerce Modules +```ts highlights={negationHighlights} +import { model } from "@medusajs/framework/utils" -Refer to [this reference](https://docs.medusajs.com/resources/commerce-modules/index.html.md) for a full list of commerce modules in Medusa. +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 +``` -## Use Commerce Modules in Custom Flows +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. -Similar to your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), the Medusa application registers a commerce module's service in the [container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md). So, you can resolve it in your custom flows. This is useful as you build unique requirements extending core commerce features. +In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`. -For example, consider you have a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) (a special function that performs a task in a series of steps with rollback mechanism) that needs a step to retrieve the total number of products. You can create a step in the workflow that resolves the Product Module's service from the container to use its methods: +### Unique Database Index -```ts highlights={highlights} -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index. -export const countProductsStep = createStep( - "count-products", - async ({ }, { container }) => { - const productModuleService = container.resolve("product") +For example: - const [,count] = await productModuleService.listAndCountProducts() +```ts highlights={uniqueHighlights} +import { model } from "@medusajs/framework/utils" - return new StepResponse(count) - } -) -``` +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + unique: true, + }, +]) -Your workflow can use services of both custom and commerce modules, supporting you in building custom flows without having to re-build core commerce features. +export default MyCustom +``` +This creates a unique composite index on the `name` and `age` properties. -# Architectural Modules -In this chapter, you’ll learn about architectural modules. +# Infer Type of Data Model -## What is an Architectural Module? +In this chapter, you'll learn how to infer the type of a data model. -An architectural module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. +## How to Infer Type of Data Model? -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. +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 `InferTypeOf` that transforms your data model to a type. -## Architectural Module Types +For example: -There are different architectural module types including: +```ts +import { InferTypeOf } from "@medusajs/framework/types" +import { MyCustom } from "../models/my-custom" // relative path to the model -![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/architectural-modules_bj9bb9.jpg) +export type MyCustom = InferTypeOf +``` -- 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. +`InferTypeOf` 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`. -## Architectural Modules List +You can now use the `MyCustom` type to reference a data model in other types, such as in workflow inputs or service method outputs: -Refer to the [Architectural Modules reference](https://docs.medusajs.com/resources/architectural-modules/index.html.md) for a list of Medusa’s architectural modules, available modules to install, and how to create an architectural module. +```ts title="Example Service" +// other imports... +import { InferTypeOf } from "@medusajs/framework/types" +import { MyCustom } from "../models/my-custom" +type MyCustom = InferTypeOf -# Module Container +class HelloModuleService extends MedusaService({ MyCustom }) { + async doSomething(): Promise { + // ... + } +} +``` -In this chapter, you'll learn about the module's container and how to resolve resources in that container. -Since modules are isolated, each module has a local container only used by the resources of that module. +# Manage Relationships -So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container. +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. -### List of Registered Resources +## Manage One-to-One Relationship -Find a list of resources or dependencies registered in a module's container in [the Container Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). +### 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}_id` property, where `{relation}` is the name of the relation property. -## Resolve Resources +For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set an email's user ID as follows: -### 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!") - } +```ts highlights={belongsHighlights} +// when creating an email +const email = await helloModuleService.createEmails({ + // other properties... + user_id: "123", +}) - // ... -} +// when updating an email +const email = await helloModuleService.updateEmails({ + id: "321", + // other properties... + user_id: "123", +}) ``` -### Loader +In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to. -A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources. +### HasOne Side -For example: +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. -```ts highlights={[["9"]]} -import { - LoaderOptions, -} from "@medusajs/framework/types" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set a user's email ID as follows: -export default async function helloWorldLoader({ - container, -}: LoaderOptions) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) +```ts highlights={hasOneHighlights} +// when creating a user +const user = await helloModuleService.createUsers({ + // other properties... + email: "123", +}) - logger.info("[helloWorldLoader]: Hello, World!") -} +// 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. -# Perform Database Operations in a Service - -In this chapter, you'll learn how to perform database operations in a module's service. +*** -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](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) instead. +## Manage One-to-Many Relationship -## Run Queries +In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. -[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. +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. -Medusa provides an `InjectManager` decorator from the Modules SDK that injects a service's method with a [forked entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager). +For example, assuming you have the [Product and Store data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-many-relationship/index.html.md), set a product's store ID as follows: -So, to run database queries in a service: +```ts highlights={manyBelongsHighlights} +// when creating a product +const product = await helloModuleService.createProducts({ + // other properties... + store_id: "123", +}) -1. Add the `InjectManager` decorator to the method. -2. Add as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator from the Modules SDK. This context holds database-related context, including the manager injected by `InjectManager` +// when updating a product +const product = await helloModuleService.updateProducts({ + id: "321", + // other properties... + store_id: "123", +}) +``` -For example, in your service, add the following methods: +In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to. -```ts highlights={methodsHighlight} -// other imports... -import { - InjectManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { SqlEntityManager } from "@mikro-orm/knex" +*** -class HelloModuleService { - // ... +## Manage Many-to-Many Relationship - @InjectManager() - async getCount( - @MedusaContext() sharedContext?: Context - ): Promise { - return await sharedContext.manager.count("my_custom") - } - - @InjectManager() - async getCountSql( - @MedusaContext() sharedContext?: Context - ): Promise { - const data = await sharedContext.manager.execute( - "SELECT COUNT(*) as num FROM my_custom" - ) - - return parseInt(data[0].num) - } -} -``` +If your many-to-many relation is represented with a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship-with-pivotentity) instead. -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. +### Create Associations -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/knex/class/EntityManager). +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. -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. +For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), set the association between products and orders as follows: -Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. +```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"], +}) +``` -## Execute Operations in Transactions +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. -To wrap database operations in a transaction, you create two methods: +### Update Associations -1. A private or protected method that's wrapped in a transaction. To wrap it in a transaction, you use the `InjectTransactionManager` decorator from the Modules SDK. -2. A public method that calls the transactional method. You use on it the `InjectManager` decorator as explained in the previous section. +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. -Both methods must accept as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator from the Modules SDK. It holds database-related contexts passed through the Medusa application. +However, this removes any existing associations to records whose IDs aren't included in the array. -For example: +For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you update the product's related orders as so: -```ts highlights={opHighlights} -import { - InjectManager, - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" +```ts +const product = await helloModuleService.updateProducts({ + id: "123", + // other properties... + orders: ["321"], +}) +``` -class HelloModuleService { - // ... - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ): Promise { - const transactionManager = sharedContext.transactionManager - await transactionManager.nativeUpdate( - "my_custom", - { - id: input.id, - }, - { - name: input.name, - } - ) +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. - // retrieve again - const updatedRecord = await transactionManager.execute( - `SELECT * FROM my_custom WHERE id = '${input.id}'` - ) +So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product: - return updatedRecord +```ts highlights={updateAssociationHighlights} +const product = await helloModuleService.retrieveProduct( + "123", + { + relations: ["orders"], } +) - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ) { - return await this.update_(input, sharedContext) - } -} +const updatedProduct = await helloModuleService.updateProducts({ + id: product.id, + // other properties... + orders: [ + ...product.orders.map((order) => order.id), + "321", + ], +}) ``` -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. +This keeps existing associations between the product and orders, and adds a new one. -The shared context's `transactionManager` property holds the transactional entity manager (injected by `InjectTransactionManager`) that you use to perform database operations. +*** -Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. +## Manage Many-to-Many Relationship with pivotEntity -### Why Wrap a Transactional Method +If your many-to-many relation is represented without a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship) instead. -The variables in the transactional method (for example, `update_`) hold values that are uncommitted to the database. They're only committed once the method finishes execution. +If you have a many-to-many relation with a `pivotEntity` specified, make sure to pass the data model representing the pivot table to [MedusaService](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that your module's service extends. -So, if in your method you perform database operations, then use their result to perform other actions, such as connecting to a third-party service, you'll be working with uncommitted data. +For example, assuming you have the [Order, Product, and OrderProduct models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), add `OrderProduct` to `MedusaService`'s object parameter: -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. +```ts highlights={["4"]} +class HelloModuleService extends MedusaService({ + Order, + Product, + OrderProduct, +}) {} +``` -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. +This will generate Create, Read, Update and Delete (CRUD) methods for the `OrderProduct` data model, which you can use to create relations between orders and products and manage the extra columns in the pivot table. -For example, the `update` method could be changed to the following: +For example: ```ts -// other imports... -import { EntityManager } from "@mikro-orm/knex" +// create order-product association +const orderProduct = await helloModuleService.createOrderProducts({ + order_id: "123", + product_id: "123", + metadata: { + test: true, + }, +}) -class HelloModuleService { - // ... - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ) { - const newData = await this.update_(input, sharedContext) +// update order-product association +const orderProduct = await helloModuleService.updateOrderProducts({ + id: "123", + metadata: { + test: false, + }, +}) - await sendNewDataToSystem(newData) +// delete order-product association +await helloModuleService.deleteOrderProducts("123") +``` - return newData +Since the `OrderProduct` data model belongs to the `Order` and `Product` data models, you can set its order and product as explained in the [one-to-many relationship section](#manage-one-to-many-relationship) using `order_id` and `product_id`. + +Refer to the [service factory reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for a full list of generated methods and their usages. + +*** + +## 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](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you retrieve a product's orders as follows: + +```ts highlights={retrieveHighlights} +const product = await helloModuleService.retrieveProducts( + "123", + { + relations: ["orders"], } -} +) ``` -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. +In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. -### Using Methods in Transactional Methods -If your transactional method uses other methods that accept a Medusa context, pass the shared context to those methods. +# Data Model’s Primary Key + +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: -```ts -// other imports... -import { EntityManager } from "@mikro-orm/knex" +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" -class HelloModuleService { +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), // ... - @InjectTransactionManager() - protected async anotherMethod( - @MedusaContext() sharedContext?: Context - ) { - // ... - } - - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ): Promise { - this.anotherMethod(sharedContext) - } -} +}) + +export default MyCustom ``` -You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context. +In the example above, the `id` property is defined as the data model's primary key. -The `anotherMethod` now runs in the same transaction as the `update_` method. -*** +# Data Model Property Types -## Configure Transactions +In this chapter, you’ll learn about the types of properties in a data model’s schema. -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. +## id -The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more. +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. -The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction. +For example: -For example, resolve the `baseRepository` in your service's constructor: +```ts highlights={idHighlights} +import { model } from "@medusajs/framework/utils" -### Extending Service Factory +const MyCustom = model.define("my_custom", { + id: model.id(), + // ... +}) -```ts highlights={[["14"]]} -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" -import { DAL } from "@medusajs/framework/types" +export default MyCustom +``` -type InjectedDependencies = { - baseRepository: DAL.RepositoryService -} +*** -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - protected baseRepository_: DAL.RepositoryService +## text - constructor({ baseRepository }: InjectedDependencies) { - super(...arguments) - this.baseRepository_ = baseRepository - } -} +The `text` method defines a string property. -export default HelloModuleService +For example: + +```ts highlights={textHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + name: model.text(), + // ... +}) + +export default MyCustom ``` -### Without Service Factory +*** -```ts highlights={[["10"]]} -import { DAL } from "@medusajs/framework/types" +## number -type InjectedDependencies = { - baseRepository: DAL.RepositoryService -} +The `number` method defines a number property. -class HelloModuleService { - protected baseRepository_: DAL.RepositoryService +For example: - constructor({ baseRepository }: InjectedDependencies) { - this.baseRepository_ = baseRepository - } -} +```ts highlights={numberHighlights} +import { model } from "@medusajs/framework/utils" -export default HelloModuleService +const MyCustom = model.define("my_custom", { + age: model.number(), + // ... +}) + +export default MyCustom ``` -Then, add the following method that uses it: +*** -```ts highlights={repoHighlights} -// ... -import { - InjectManager, - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" +## float -class HelloModuleService { - // ... - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ): Promise { - return await this.baseRepository_.transaction( - async (transactionManager) => { - await transactionManager.nativeUpdate( - "my_custom", - { - id: input.id, - }, - { - name: input.name, - } - ) +This property is only available after [Medusa v2.1.2](https://github.com/medusajs/medusa/releases/tag/v2.1.2). - // retrieve again - const updatedRecord = await transactionManager.execute( - `SELECT * FROM my_custom WHERE id = '${input.id}'` - ) +The `float` method defines a number property that allows for values with decimal places. - return updatedRecord - }, - { - transaction: sharedContext.transactionManager, - } - ) - } +Use this property type when it's less important to have high precision for numbers with large decimal places. Alternatively, for higher percision, use the [bigNumber property](#bignumber). - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ) { - return await this.update_(input, sharedContext) - } -} -``` +For example: -The `update_` method uses the `baseRepository_.transaction` method to wrap a function in a transaction. +```ts highlights={floatHighlights} +import { model } from "@medusajs/framework/utils" -The function parameter receives a transactional entity manager as a parameter. Use it to perform the database operations. +const MyCustom = model.define("my_custom", { + rating: model.float(), + // ... +}) -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. +export default MyCustom +``` -Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. +*** -### Transaction Options +## bigNumber -The second parameter of the `baseRepository_.transaction` method is an object of options that accepts the following properties: +The `bigNumber` method defines a number property that expects large numbers, such as prices. -1. `transaction`: Set the transactional entity manager passed to the function. You must provide this option as explained in the previous section. +Use this property type when it's important to have high precision for numbers with large decimal places. Alternatively, for less percision, use the [float property](#float). -```ts highlights={[["16"]]} -// other imports... -import { EntityManager } from "@mikro-orm/knex" +For example: -class HelloModuleService { - // ... - @InjectTransactionManager() - async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ): Promise { - return await this.baseRepository_.transaction( - 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" +```ts highlights={bigNumberHighlights} +import { model } from "@medusajs/framework/utils" -class HelloModuleService { +const MyCustom = model.define("my_custom", { + price: model.bigNumber(), // ... - @InjectTransactionManager() - async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ): Promise { - return await this.baseRepository_.transaction( - 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 - ): Promise { - return await this.baseRepository_.transaction( - async (transactionManager) => { - // ... - }, - { - enableNestedTransactions: false, - } - ) - } -} +export default MyCustom ``` +*** -# Module Isolation +## boolean -In this chapter, you'll learn how modules are isolated, and what that means for your custom development. +The `boolean` method defines a boolean property. -- 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](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), to extend a module's data models and retrieve data across modules. +For example: -## How are Modules Isolated? +```ts highlights={booleanHighlights} +import { model } from "@medusajs/framework/utils" -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. +const MyCustom = model.define("my_custom", { + hasAccount: model.boolean(), + // ... +}) -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. +export default MyCustom +``` *** -## Why are Modules Isolated +### enum -Some of the module isolation's benefits include: +The `enum` method defines a property whose value can only be one of the specified values. -- 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. +For example: -*** +```ts highlights={enumHighlights} +import { model } from "@medusajs/framework/utils" -## How to Extend Data Model of Another Module? +const MyCustom = model.define("my_custom", { + color: model.enum(["black", "white"]), + // ... +}) -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](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). +export default MyCustom +``` -*** +The `enum` method accepts an array of possible string values. -## 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. +## dateTime -Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. +The `dateTime` method defines a timestamp property. -### Example +For example: -For example, consider you have two modules: +```ts highlights={dateTimeHighlights} +import { model } from "@medusajs/framework/utils" -1. A module that stores and manages brands in your application. -2. A module that integrates a third-party Content Management System (CMS). +const MyCustom = model.define("my_custom", { + date_of_birth: model.dateTime(), + // ... +}) -To sync brands from your application to the third-party system, create the following steps: +export default MyCustom +``` -```ts title="Example Steps" highlights={stepsHighlights} -const retrieveBrandsStep = createStep( - "retrieve-brands", - async (_, { container }) => { - const brandModuleService = container.resolve( - "brandModuleService" - ) +*** - const brands = await brandModuleService.listBrands() +## json - return new StepResponse(brands) - } -) +The `json` method defines a property whose value is a stringified JSON object. -const createBrandsInCmsStep = createStep( - "create-brands-in-cms", - async ({ brands }, { container }) => { - const cmsModuleService = container.resolve( - "cmsModuleService" - ) +For example: - const cmsBrands = await cmsModuleService.createBrands(brands) +```ts highlights={jsonHighlights} +import { model } from "@medusajs/framework/utils" - return new StepResponse(cmsBrands, cmsBrands) - }, - async (brands, { container }) => { - const cmsModuleService = container.resolve( - "cmsModuleService" - ) +const MyCustom = model.define("my_custom", { + metadata: model.json(), + // ... +}) - await cmsModuleService.deleteBrands( - brands.map((brand) => brand.id) - ) - } -) +export default MyCustom ``` -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: +## array -```ts title="Example Workflow" -export const syncBrandsWorkflow = createWorkflow( - "sync-brands", - () => { - const brands = retrieveBrandsStep() +The `array` method defines an array of strings property. - createBrandsInCmsStep({ brands }) - } -) -``` +For example: -You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. +```ts highlights={arrHightlights} +import { model } from "@medusajs/framework/utils" +const MyCustom = model.define("my_custom", { + names: model.array(), + // ... +}) -# Loaders +export default MyCustom +``` -In this chapter, you’ll learn about loaders and how to use them. +*** -## What is a Loader? +## Properties Reference -When building a commerce application, you'll often need to execute an action the first time the application starts. For example, if your application needs to connect to databases other than Medusa's PostgreSQL database, you might need to establish a connection on application startup. +Refer to the [Data Model API reference](https://docs.medusajs.com/resources/references/data-model/index.html.md) for a full reference of the properties. -In Medusa, you can execute an action when the application starts using a loader. A loader is a function exported by a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of business logic for a single domain. When the Medusa application starts, it executes all loaders exported by configured modules. -Loaders are useful to register custom resources, such as database connections, in the [module's container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md), which is similar to the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) but includes only [resources available to the module](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). Modules are isolated, so they can't access resources outside of them, such as a service in another module. +# Data Model Relationships -Medusa isolates modules to ensure that they're re-usable across applications, aren't tightly coupled to other resources, and don't have implications when integrated into the Medusa application. Learn more about why modules are isolated in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), and check out [this reference for the list of resources in the module's container](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). +In this chapter, you’ll learn how to define relationships between data models in your module. -*** +## What is a Relationship Property? -## How to Create a Loader? +A relationship property defines an association in the database between two models. It's created using the Data Model Language (DML) methods, such as `hasOne` or `belongsTo`. -### 1. Implement Loader Function +When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type. -You create a loader function in a TypeScript or JavaScript file under a module's `loaders` directory. +You want to create a relation between data models in the same module. -For example, consider you have a `hello` module, you can create a loader at `src/modules/hello/loaders/hello-world.ts` with the following content: +You want to create a relationship between data models in different modules. Use module links instead. -![Example of loader file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732865671/Medusa%20Book/loader-dir-overview_eg6vtu.jpg) +*** -Learn how to create a module in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +## One-to-One Relationship -```ts title="src/modules/hello/loaders/hello-world.ts" -import { - LoaderOptions, -} from "@medusajs/framework/types" +A one-to-one relationship indicates that one record of a data model belongs to or is associated with another. -export default async function helloWorldLoader({ - container, -}: LoaderOptions) { - const logger = container.resolve("logger") +To define a one-to-one relationship, create relationship properties in the data models using the following methods: - logger.info("[helloWorldLoader]: Hello, World!") -} -``` +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. -The loader file exports an async function, which is the function executed when the application loads. +For example: -The function receives an object parameter that has a `container` property, which is the module's container that you can use to resolve resources from. In this example, you resolve the Logger utility to log a message in the terminal. +```ts highlights={oneToOneHighlights} +import { model } from "@medusajs/framework/utils" -Find the list of resources in the module's container in [this reference](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email), +}) -### 2. Export Loader in Module Definition - -After implementing the loader, you must export it in the module's definition in the `index.ts` file at the root of the module's directory. Otherwise, the Medusa application will not run it. - -So, to export the loader you implemented above in the `hello` module, add the following to `src/modules/hello/index.ts`: - -```ts title="src/modules/hello/index.ts" -// other imports... -import helloWorldLoader from "./loaders/hello-world" - -export default Module("hello", { - // ... - loaders: [helloWorldLoader], +const Email = model.define("email", { + id: model.id().primaryKey(), + user: model.belongsTo(() => User, { + mappedBy: "email", + }), }) ``` -The second parameter of the `Module` function accepts a `loaders` property whose value is an array of loader functions. The Medusa application will execute these functions when it starts. - -### Test the Loader - -Assuming your module is [added to Medusa's configuration](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you can test the loader by starting the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, you'll find the following message logged in the terminal: - -```plain -info: [HELLO MODULE] Just started the Medusa application! -``` +In the example above, a user has one email, and an email belongs to one user. -This indicates that the loader in the `hello` module ran and logged this message. +The `hasOne` and `belongsTo` methods accept a function as the 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. -## Example: Register Custom MongoDB Connection +### Optional Relationship -As mentioned in this chapter's introduction, loaders are most useful when you need to register a custom resource in the module's container to re-use it in other customizations in the module. +To make the relationship optional on the `hasOne` or `belongsTo` side, use the `nullable` method on either property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/configure-properties#nullable-property/index.html.md). -Consider your have a MongoDB module that allows you to perform operations on a MongoDB database. +### One-sided One-to-One Relationship -### Prerequisites +If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. -- [MongoDB database that you can connect to from a local machine.](https://www.mongodb.com) -- [Install the MongoDB SDK in your Medusa application.](https://www.mongodb.com/docs/drivers/node/current/quick-start/download-and-install/#install-the-node.js-driver) +For example: -To connect to the database, you create the following loader in your module: +```ts highlights={oneToOneUndefinedHighlights} +import { model } from "@medusajs/framework/utils" -```ts title="src/modules/mongo/loaders/connection.ts" highlights={loaderHighlights} -import { LoaderOptions } from "@medusajs/framework/types" -import { asValue } from "awilix" -import { MongoClient } from "mongodb" +const User = model.define("user", { + id: model.id().primaryKey(), +}) -type ModuleOptions = { - connection_url?: string - db_name?: string -} +const Email = model.define("email", { + id: model.id().primaryKey(), + user: model.belongsTo(() => User, { + mappedBy: undefined, + }), +}) +``` -export default async function mongoConnectionLoader({ - container, - options, -}: LoaderOptions) { - if (!options.connection_url) { - throw new Error(`[MONGO MDOULE]: connection_url option is required.`) - } - if (!options.db_name) { - throw new Error(`[MONGO MDOULE]: db_name option is required.`) - } - const logger = container.resolve("logger") - - try { - const clientDb = ( - await (new MongoClient(options.connection_url)).connect() - ).db(options.db_name) +### One-to-One Relationship in the Database - logger.info("Connected to MongoDB") +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: - container.register( - "mongoClient", - asValue(clientDb) - ) - } catch (e) { - logger.error( - `[MONGO MDOULE]: An error occurred while connecting to MongoDB: ${e}` - ) - } -} -``` +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. -The loader function accepts in its object parameter an `options` property, which is the options passed to the module in Medusa's configurations. For example: +![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) -```ts title="medusa-config.ts" highlights={optionHighlights} -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/mongo", - options: { - connection_url: process.env.MONGO_CONNECTION_URL, - db_name: process.env.MONGO_DB_NAME, - }, - }, - ], -}) -``` +*** -Passing options is useful when your module needs informations like connection URLs or API keys, as it ensures your module can be re-usable across applications. For the MongoDB Module, you expect two options: +## One-to-Many Relationship -- `connection_url`: the URL to connect to the MongoDB database. -- `db_name`: The name of the database to connect to. +A one-to-many relationship indicates that one record of a data model has many records of another data model. -In the loader, you check first that these options are set before proceeding. Then, you create an instance of the MongoDB client and connect to the database specified in the options. +To define a one-to-many relationship, create relationship properties in the data models using the following methods: -After creating the client, you register it in the module's container using the container's `register` method. The method accepts two parameters: +1. `hasMany`: indicates that the model has more than one record of the specified model. +2. `belongsTo`: indicates that the model belongs to one record of the specified model. -1. The key to register the resource under, which in this case is `mongoClient`. You'll use this name later to resolve the client. -2. The resource to register in the container, which is the MongoDB client you created. However, you don't pass the resource as-is. Instead, you need to use an `asValue` function imported from the [awilix package](https://github.com/jeffijoe/awilix), which is the package used to implement the container functionality in Medusa. +For example: -### Use Custom Registered Resource in Module's Service +```ts highlights={oneToManyHighlights} +import { model } from "@medusajs/framework/utils" -After registering the custom MongoDB client in the module's container, you can now resolve and use it in the module's service. +const Store = model.define("store", { + id: model.id().primaryKey(), + products: model.hasMany(() => Product), +}) -For example: +const Product = model.define("product", { + id: model.id().primaryKey(), + store: model.belongsTo(() => Store, { + mappedBy: "products", + }), +}) +``` -```ts title="src/modules/mongo/service.ts" -import type { Db } from "mongodb" +In this example, a store has many products, but a product belongs to one store. -type InjectedDependencies = { - mongoClient: Db -} +### Optional Relationship -export default class MongoModuleService { - private mongoClient_: Db +To make the relationship optional on the `belongsTo` side, use the `nullable` method on the property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/configure-properties#nullable-property/index.html.md). - constructor({ mongoClient }: InjectedDependencies) { - this.mongoClient_ = mongoClient - } +### One-to-Many Relationship in the Database - async createMovie({ title }: { - title: string - }) { - const moviesCol = this.mongoClient_.collection("movie") +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: - const insertedMovie = await moviesCol.insertOne({ - title, - }) +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. - const movie = await moviesCol.findOne({ - _id: insertedMovie.insertedId, - }) +![Diagram illustrating the relation between a store and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733937/Medusa%20Book/one-to-many_d6wtcw.jpg) - return movie - } +*** - async deleteMovie(id: string) { - const moviesCol = this.mongoClient_.collection("movie") +## Many-to-Many Relationship - await moviesCol.deleteOne({ - _id: { - equals: id, - }, - }) - } -} -``` +A many-to-many relationship indicates that many records of a data model can be associated with many records of another data model. -The service `MongoModuleService` resolves the `mongoClient` resource you registered in the loader and sets it as a class property. You then use it in the `createMovie` and `deleteMovie` methods, which create and delete a document in a `movie` collection in the MongoDB database, respectively. +To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method. -Make sure to export the loader in the module's definition in the `index.ts` file at the root directory of the module: +For example: -```ts title="src/modules/mongo/index.ts" highlights={[["9"]]} -import { Module } from "@medusajs/framework/utils" -import MongoModuleService from "./service" -import mongoConnectionLoader from "./loaders/connection" +```ts highlights={manyToManyHighlights} +import { model } from "@medusajs/framework/utils" -export const MONGO_MODULE = "mongo" +const Order = model.define("order", { + id: model.id().primaryKey(), + products: model.manyToMany(() => Product, { + mappedBy: "orders", + pivotTable: "order_product", + joinColumn: "order_id", + inverseJoinColumn: "product_id", + }), +}) -export default Module(MONGO_MODULE, { - service: MongoModuleService, - loaders: [mongoConnectionLoader], +const Product = model.define("product", { + id: model.id().primaryKey(), + orders: model.manyToMany(() => Order, { + mappedBy: "products", + }), }) ``` -### Test it Out +The `manyToMany` method accepts two parameters: -You can test the connection out by starting the Medusa application. If it's successful, you'll see the following message logged in the terminal: +1. A function that returns the associated data model. +2. An object of optional configuration. Only one of the data models in the relation can define the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations, and it's considered the owner data model. The object can accept the following properties: + - `mappedBy`: The name of the relationship property in the other data model. If not set, the property's name is inferred from the associated data model's name. + - `pivotTable`: The name of the pivot table created in the database for the many-to-many relation. If not set, the pivot table is inferred by combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`. + - `joinColumn`: The name of the column in the pivot table that points to the owner model's primary key. + - `inverseJoinColumn`: The name of the column in the pivot table that points to the owned model's primary key. -```bash -info: Connected to MongoDB -``` +The `pivotTable`, `joinColumn`, and `inverseJoinColumn` properties are only available after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). -You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database. +Following [Medusa v2.1.0](https://github.com/medusajs/medusa/releases/tag/v2.1.0), if `pivotTable`, `joinColumn`, and `inverseJoinColumn` aren't specified on either model, the owner is decided based on alphabetical order. So, in the example above, the `Order` data model would be the owner. +In this example, an order is associated with many products, and a product is associated with many orders. Since the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations are defined on the order, it's considered the owner data model. -# Modules Directory Structure +### Many-to-Many Relationship in the Database -In this document, you'll learn about the expected files and directories in your module. +When you generate the migrations of data models that have a many-to-many relationship, the migration adds a new pivot table. Its name is either the name you specify in the `pivotTable` configuration or the inferred name combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`. -![Module Directory Structure Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1714379976/Medusa%20Book/modules-dir-overview_nqq7ne.jpg) +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. -## index.ts +The pivot table has columns with foreign keys pointing to the primary key of the associated tables. The column's name is either: -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](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +- The value of the `joinColumn` configuration for the owner table, and the `inverseJoinColumn` configuration for the owned table; +- Or the inferred name `{table_name}_id`. -*** +![Diagram illustrating the relation between order and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726734269/Medusa%20Book/many-to-many_fzy5pq.jpg) -## service.ts +### Many-To-Many with Custom Columns -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](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +To add custom columns to the pivot table between two data models having a many-to-many relationship, you must define a new data model that represents the pivot table. -*** +For example: -## Other Directories +```ts highlights={manyToManyColumnHighlights} +import { model } from "@medusajs/framework/utils" -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. +export const Order = model.define("order_test", { + id: model.id().primaryKey(), + products: model.manyToMany(() => Product, { + pivotEntity: () => OrderProduct, + }), +}) +export const Product = model.define("product_test", { + id: model.id().primaryKey(), + orders: model.manyToMany(() => Order), +}) -# Module Options +export const OrderProduct = model.define("orders_products", { + id: model.id().primaryKey(), + order: model.belongsTo(() => Order, { + mappedBy: "products", + }), + product: model.belongsTo(() => Product, { + mappedBy: "orders", + }), + metadata: model.json().nullable(), +}) +``` -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. +The `Order` and `Product` data models have a many-to-many relationship. To add extra columns to the created pivot table, you pass a `pivotEntity` option to the `products` relation in `Order` (since `Order` is the owner). The value of `pivotEntity` is a function that returns the data model representing the pivot table. -## What are Module Options? +The `OrderProduct` model defines, aside from the ID, the following properties: -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. +- `order`: A relation that indicates this model belongs to the `Order` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Order` data model. +- `product`: A relation that indicates this model belongs to the `Product` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Product` data model. +- `metadata`: An extra column to add to the pivot table of type `json`. You can add other columns as well to the model. *** -## 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`. +## Set Relationship Name in the Other Model -For example: +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. -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/hello", - options: { - capitalize: true, - }, - }, - ], -}) -``` +This is useful if the relationship property’s name is different from that of the associated data model. -The `options` property’s value is an object. You can pass any properties you want. +As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. -### Pass Options to a Module in a Plugin +For example: -If your module is part of a plugin, you can pass options to the module in the plugin’s configuration. +```ts highlights={relationNameHighlights} +import { model } from "@medusajs/framework/utils" -For example: +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email, { + mappedBy: "owner", + }), +}) -```ts title="medusa-config.ts" -import { defineConfig } from "@medusajs/framework/utils" -module.exports = defineConfig({ - plugins: [ - { - resolve: "@myorg/plugin-name", - options: { - capitalize: true, - }, - }, - ], +const Email = model.define("email", { + id: model.id().primaryKey(), + owner: model.belongsTo(() => User, { + mappedBy: "email", + }), }) ``` -The `options` property in the plugin configuration is passed to all modules in a plugin. +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`. *** -## Access Module Options in Main Service - -The module’s main service receives the module options as a second parameter. +## Cascades -For example: +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. -```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" +For example, if a store is deleted, its products should also be deleted. -// recommended to define type in another file -type ModuleOptions = { - capitalize?: boolean -} +The `cascades` method used on a data model configures which child records an operation is cascaded to. -export default class HelloModuleService extends MedusaService({ - MyCustom, -}){ - protected options_: ModuleOptions +For example: - constructor({}, options?: ModuleOptions) { - super(...arguments) +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" - this.options_ = options || { - capitalize: false, - } - } +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. -## Access Module Options in Loader +In the example above, when a store is deleted, its associated products are also deleted. -The object that a module’s loaders receive as a parameter has an `options` property holding the module's options. -For example: +# Searchable Data Model Property -```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" +In this chapter, you'll learn what a searchable property is and how to define it. -// recommended to define type in another file -type ModuleOptions = { - capitalize?: boolean -} +## What is a Searchable Property? -export default async function helloWorldLoader({ - options, -}: LoaderOptions) { - - console.log( - "[HELLO MODULE] Just started the Medusa application!", - options - ) -} -``` +Methods generated by the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) 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. -## Validate Module Options +*** -If you expect a certain option and want to throw an error if it's not provided or isn't valid, it's recommended to perform the validation in a loader. The module's service is only instantiated when it's used, whereas the loader runs the when the Medusa application starts. +## Define a Searchable Property -So, by performing the validation in the loader, you ensure you can throw an error at an early point, rather than when the module is used. +Use the `searchable` method on a `text` property to indicate that it's searchable. -For example, to validate that the Hello Module received an `apiKey` option, create the loader `src/modules/loaders/validate.ts`: +For example: -```ts title="src/modules/hello/loaders/validate.ts" -import { LoaderOptions } from "@medusajs/framework/types" -import { MedusaError } from "@medusajs/framework/utils" +```ts highlights={searchableHighlights} +import { model } from "@medusajs/framework/utils" -// recommended to define type in another file -type ModuleOptions = { - apiKey?: string -} +const MyCustom = model.define("my_custom", { + name: model.text().searchable(), + // ... +}) -export default async function validationLoader({ - options, -}: LoaderOptions) { - if (!options.apiKey) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Hello Module requires an apiKey option." - ) - } -} +export default MyCustom ``` -Then, export the loader in the module's definition file, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md): +In this example, the `name` property is searchable. -```ts title="src/modules/hello/index.ts" -// other imports... -import validationLoader from "./loaders/validate" +### Search Example -export default Module("hello", { - // ... - loaders: [validationLoader], +If you pass a `q` filter to the `listMyCustoms` method: + +```ts +const myCustoms = await helloModuleService.listMyCustoms({ + q: "John", }) ``` -Now, when the Medusa application starts, the loader will run, validating the module's options and throwing an error if the `apiKey` option is missing. +This retrieves records that include `John` in their `name` property. -# Multiple Services in a Module +# Write Migration -In this chapter, you'll learn how to use multiple services in a module. +In this chapter, you'll learn how to create a migration and write it manually. -## Module's Main and Internal Services +## What is a Migration? -A module has one main service only, which is the service exported in the module's definition. +A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It has two methods: -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. +- The `up` method reflects changes on the database. +- The `down` method reverts the changes made in the `up` method. *** -## How to Add an Internal Service +## How to Write a Migration? -### 1. Create Service +The Medusa CLI tool provides a [db:generate](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbgenerate/index.html.md) command to generate a migration for the specified modules' data models. -To add an internal service, create it in the `services` directory of your module. +Alternatively, you can manually create a migration file under the `migrations` directory of your module. -For example, create the file `src/modules/hello/services/client.ts` with the following content: +For example: -```ts title="src/modules/hello/services/client.ts" -export class ClientService { - async getMessage(): Promise { - return "Hello, World!" - } -} -``` +```ts title="src/modules/blog/migrations/Migration20240429.ts" +import { Migration } from "@mikro-orm/migrations" -### 2. Export Service in Index +export class Migration20240702105919 extends Migration { -Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. + async up(): Promise { + this.addSql("create table if not exists \"author\" (\"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 \"author_pkey\" primary key (\"id\"));") + } -For example, create the file `src/modules/hello/services/index.ts` with the following content: + async down(): Promise { + this.addSql("drop table if exists \"author\" cascade;") + } -```ts title="src/modules/hello/services/index.ts" -export * from "./client" +} ``` -This exports the `ClientService`. +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`. -### 3. Resolve Internal Service +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. -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. +In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted. -For example, in your main service: +Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. -```ts title="src/modules/hello/service.ts" highlights={[["5"], ["13"]]} -// other imports... -import { ClientService } from "./services" +*** -type InjectedDependencies = { - clientService: ClientService -} +## Run the Migration -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - protected clientService_: ClientService +To run your migration, run the following command: - constructor({ clientService }: InjectedDependencies) { - super(...arguments) - this.clientService_ = clientService - } -} +This command also syncs module links. If you don't want that, use the `--skip-links` option. + +```bash +npx medusa db:migrate ``` -You can now use your internal service in your main service. +This reflects the changes in the database as implemented in the migration's `up` method. *** -## Resolve Resources in Internal Service +## Rollback the Migration -Resolve dependencies from your module's container in the constructor of your internal service. +To rollback or revert the last migration you ran for a module, run the following command: -For example: +```bash +npx medusa db:rollback blog +``` -```ts -import { Logger } from "@medusajs/framework/types" +This rolls back the last ran migration on the Blog Module. -type InjectedDependencies = { - logger: Logger -} +### Caution: Rollback Migration before Deleting -export class ClientService { - protected logger_: Logger +If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations. - constructor({ logger }: InjectedDependencies) { - this.logger_ = logger - } -} -``` +For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors. + +So, always rollback the migration before deleting it. *** -## Access Module Options +## More Database Commands -Your internal service can't access the module's options. +To learn more about the Medusa CLI's database commands, refer to [this CLI reference](https://docs.medusajs.com/resources/medusa-cli/commands/db/index.html.md). -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: +# Create a Plugin -```ts -import { ConfigModule } from "@medusajs/framework/types" -import { HELLO_MODULE } from ".." +In this chapter, you'll learn how to create a Medusa plugin and publish it. -export type InjectedDependencies = { - configModule: ConfigModule -} +A [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) is a package of reusable Medusa customizations that you can install in any Medusa application. By creating and publishing a plugin, you can reuse your Medusa customizations across multiple projects or share them with the community. -export class ClientService { - protected options: Record +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). - constructor({ configModule }: InjectedDependencies) { - const moduleDef = configModule.modules[HELLO_MODULE] +## 1. Create a Plugin Project - if (typeof moduleDef !== "boolean") { - this.options = moduleDef.options - } - } -} -``` +Plugins are created in a separate Medusa project. This makes the development and publishing of the plugin easier. Later, you'll install that plugin in your Medusa application to test it out and use it. -The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. +Medusa's `create-medusa-app` CLI tool provides the option to create a plugin project. Run the following command to create a new plugin project: -If its value is not a `boolean`, set the service's options to the module configuration's `options` property. +```bash +npx create-medusa-app my-plugin --plugin +``` +This will create a new Medusa plugin project in the `my-plugin` directory. -# Service Constraints +### Plugin Directory Structure -This chapter lists constraints to keep in mind when creating a service. +After the installation is done, the plugin structure will look like this: -## Use Async Methods +![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) -Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronous, it always executes methods in the wrapper with the `await` keyword. +- `src/`: Contains the Medusa customizations. +- `src/admin`: Contains [admin extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). +- `src/api`: Contains [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) and [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). You can add store, admin, or any custom API routes. +- `src/jobs`: Contains [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). +- `src/links`: Contains [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). +- `src/modules`: Contains [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +- `src/provider`: Contains [module providers](#create-module-providers). +- `src/subscribers`: Contains [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). +- `src/workflows`: Contains [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). You can also add [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) under `src/workflows/hooks`. +- `package.json`: Contains the plugin's package information, including general information and dependencies. +- `tsconfig.json`: Contains the TypeScript configuration for the plugin. -For example, if you have a synchronous `getMessage` method, and you use it in other resources like workflows, Medusa executes it as an async method: +*** -```ts -await helloModuleService.getMessage() -``` +## 2. Prepare Plugin -So, make sure your service's methods are always async to avoid unexpected errors or behavior. +### Package Name -```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" +Before developing, testing, and publishing your plugin, make sure its name in `package.json` is correct. This is the name you'll use to install the plugin in your Medusa application. -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - // Don't - getMessage(): string { - return "Hello, World!" - } +For example: - // Do - async getMessage(): Promise { - return "Hello, World!" - } +```json title="package.json" +{ + "name": "@myorg/plugin-name", + // ... } - -export default HelloModuleService ``` +### Package Keywords -# Service Factory - -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. - -Your service provides data-management functionalities of your data models. +In addition, make sure that the `keywords` field in `package.json` includes the keyword `medusa-plugin` and `medusa-v2`. This helps Medusa list community plugins on the Medusa website: -*** +```json title="package.json" +{ + "keywords": [ + "medusa-plugin", + "medusa-v2" + ], + // ... +} +``` -## How to Extend the Service Factory? +### Package Dependencies -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. +Your plugin project will already have the dependencies mentioned in this section. If you haven't made any changes to the dependencies, you can skip this section. -For example, create the file `src/modules/hello/service.ts` with the following content: +In the `package.json` file you must have the Medusa dependencies as `devDependencies` and `peerDependencies`. In addition, you must have `@swc/core` as a `devDependency`, as it's used by the plugin CLI tools. -```ts title="src/modules/hello/service.ts" highlights={highlights} -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" +For example, assuming `2.5.0` is the latest Medusa version: -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - // TODO implement custom methods +```json title="package.json" +{ + "devDependencies": { + "@medusajs/admin-sdk": "2.5.0", + "@medusajs/cli": "2.5.0", + "@medusajs/framework": "2.5.0", + "@medusajs/medusa": "2.5.0", + "@medusajs/test-utils": "2.5.0", + "@medusajs/ui": "4.0.4", + "@medusajs/icons": "2.5.0", + "@swc/core": "1.5.7", + }, + "peerDependencies": { + "@medusajs/admin-sdk": "2.5.0", + "@medusajs/cli": "2.5.0", + "@medusajs/framework": "2.5.0", + "@medusajs/test-utils": "2.5.0", + "@medusajs/medusa": "2.5.0", + "@medusajs/ui": "4.0.3", + "@medusajs/icons": "2.5.0", + } } - -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 +## 3. Publish Plugin Locally for Development and Testing -The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database. +Medusa's CLI tool provides commands to simplify developing and testing your plugin in a local Medusa application. You start by publishing your plugin in the local package registry, then install it in your Medusa application. You can then watch for changes in the plugin as you develop it. -The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`. +### Publish and Install Local Package -For example, the following methods are generated for the service above: +### Prerequisites -Find a complete reference of each of the methods in [this documentation](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) +- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) -### listMyCustoms +The first time you create your plugin, you need to publish the package into a local package registry, then install it in your Medusa application. This is a one-time only process. -### listMyCustoms +To publish the plugin to the local registry, run the following command in your plugin project: -This method retrieves an array of records based on filters and pagination configurations. +```bash title="Plugin project" +npx medusa plugin:publish +``` -For example: +This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. -```ts -const myCustoms = await helloModuleService - .listMyCustoms() +Next, navigate to your Medusa application: -// with filters -const myCustoms = await helloModuleService - .listMyCustoms({ - id: ["123"] - }) +```bash title="Medusa application" +cd ~/path/to/medusa-app ``` -### listAndCount +Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. -### retrieveMyCustom +Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: -This method retrieves a record by its ID. +```bash npm2yarn title="Medusa application" +npm install --save-dev yalc +``` -For example: +After that, run the following Medusa CLI command to install the plugin: -```ts -const myCustom = await helloModuleService - .retrieveMyCustom("123") +```bash title="Medusa application" +npx medusa plugin:add @myorg/plugin-name ``` -### retrieveMyCustom +Make sure to replace `@myorg/plugin-name` with the name of your plugin as specified in `package.json`. Your plugin will be installed from the local package registry into your Medusa application. -### updateMyCustoms +### Register Plugin in Medusa Application -This method updates and retrieves records of the data model. +After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. -For example: +Add the plugin to the `plugins` array in the `medusa-config.ts` file: -```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([ +```ts title="medusa-config.ts" highlights={pluginHighlights} +module.exports = defineConfig({ + // ... + plugins: [ { - selector: { - id: ["123", "321"] - }, - data: { - name: "test" - } + resolve: "@myorg/plugin-name", + options: {}, }, - ]) + ], +}) ``` -### createMyCustoms +The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package. -### softDeleteMyCustoms +#### Pass Module Options through Plugin -This method soft-deletes records using an array of IDs or an object of filters. +Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. For example: -```ts -await helloModuleService.softDeleteMyCustoms("123") - -// soft-delete multiple -await helloModuleService.softDeleteMyCustoms([ - "123", "321" -]) - -// use filters -await helloModuleService.softDeleteMyCustoms({ - id: ["123", "321"] +```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: { + apiKey: true, + }, + }, + ], }) ``` -### updateMyCustoms +The `options` property in the plugin configuration is passed to all modules in the plugin. Learn more about module options in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). -### deleteMyCustoms +### Watch Plugin Changes During Development -### softDeleteMyCustoms +While developing your plugin, you can watch for changes in the plugin and automatically update the plugin in the Medusa application using it. This is the only command you'll continuously need during your plugin development. -### restoreMyCustoms +To do that, run the following command in your plugin project: -### Using a Constructor +```bash title="Plugin project" +npx medusa plugin:develop +``` -If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`. +This command will: -For example: +- Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built. +- Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions. -```ts highlights={[["8"]]} -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" +### Start Medusa Application -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - constructor() { - super(...arguments) - } -} +You can start your Medusa application's development server to test out your plugin: -export default HelloModuleService +```bash npm2yarn title="Medusa application" +npm run dev ``` +While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. -# Access Workflow Errors - -In this chapter, you’ll learn how to access errors that occur during a workflow’s execution. +*** -## How to Access Workflow Errors? +## 4. Create Customizations in the Plugin -By default, when an error occurs in a workflow, it throws that error, and the execution stops. +You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. -You can configure the workflow to return the errors instead so that you can access and handle them differently. +- [Create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) +- [Create a module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) +- [Create a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) +- [Add a workflow hook](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) +- [Create an API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) +- [Add a subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) +- [Add a scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) +- [Add an admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) +- [Add an admin UI route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) -For example: +While building those customizations, you can test them in your Medusa application by [watching the plugin changes](#watch-plugin-changes-during-development) and [starting the Medusa application](#start-medusa-application). -```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" +### Generating Migrations for Modules -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result, errors } = await myWorkflow(req.scope) - .run({ - // ... - throwOnError: false, - }) +During your development, you may need to generate migrations for modules in your plugin. To do that, use the `plugin:db:generate` command: - if (errors.length) { - return res.send({ - errors: errors.map((error) => error.error), - }) - } +```bash title="Plugin project" +npx medusa plugin:db:generate +``` - res.send(result) -} +This command generates migrations for all modules in the plugin. You can then run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command: +```bash title="Medusa application" +npx medusa db:migrate ``` -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. +### Importing Module Resources -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. +Your plugin project should have the following exports in `package.json`: +```json title="package.json" +{ + "exports": { + "./package.json": "./package.json", + "./workflows": "./.medusa/server/src/workflows/index.js", + "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js", + "./providers/*": "./.medusa/server/src/providers/*/index.js", + "./*": "./.medusa/server/src/*.js" + } +} +``` -# Expose a Workflow Hook +Aside from the `./package.json` and `./providers`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export. -In this chapter, you'll learn how to expose a hook in your workflow. +The plugin exports the following files and directories: -## When to Expose a Hook +- `./package.json`: The package.json file. Medusa needs to access the `package.json` when registering the plugin. +- `./workflows`: The workflows exported in `./src/workflows/index.ts`. +- `./.medusa/server/src/modules/*`: The definition file of modules. This is useful if you create links to the plugin's modules in the Medusa application. +- `./providers/*`: The definition file of module providers. This allows you to register the plugin's providers in the Medusa application. +- `./*`: Any other files in the plugin's `src` directory. -Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow. +With these exports, you can import the plugin's resources in the Medusa application's code like this: -Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead. +`@myorg/plugin-name` is the plugin package's name. -*** +```ts +import { Workflow1, Workflow2 } from "@myorg/plugin-name/workflows" +import BlogModule from "@myorg/plugin-name/modules/blog" +// import other files created in plugin like ./src/types/blog.ts +import BlogType from "@myorg/plugin-name/types/blog" +``` -## How to Expose a Hook in a Workflow? +And you can register a module provider in the Medusa application's `medusa-config.ts` like this: -To expose a hook in your workflow, use `createHook` from the Workflows SDK. +```ts highlights={[["9"]]} title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + { + resolve: "@myorg/plugin-name/providers/my-notification", + id: "my-notification", + options: { + channels: ["email"], + // provider options... + }, + }, + ], + }, + }, + ], +}) +``` -For example: +You pass to `resolve` the path to the provider relative to the plugin package. So, in this example, the `my-notification` provider is located in `./src/providers/my-notification/index.ts` of the plugin. -```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" +### Create Module Providers -export const myWorkflow = createWorkflow( - "my-workflow", - function (input) { - const product = createProductStep(input) - const productCreatedHook = createHook( - "productCreated", - { productId: product.id } - ) +To learn how to create module providers, refer to the following guides: - return new WorkflowResponse(product, { - hooks: [productCreatedHook], - }) - } -) -``` +- [File Module Provider](https://docs.medusajs.com/resources/references/file-provider-module/index.html.md) +- [Notification Module Provider](https://docs.medusajs.com/resources/references/notification-provider-module/index.html.md) +- [Auth Module Provider](https://docs.medusajs.com/resources/references/auth/provider/index.html.md) +- [Payment Module Provider](https://docs.medusajs.com/resources/references/payment/provider/index.html.md) +- [Fulfillment Module Provider](https://docs.medusajs.com/resources/references/fulfillment/provider/index.html.md) +- [Tax Module Provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md) -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. +## 5. Publish Plugin to NPM -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. +Medusa's CLI tool provides a command that bundles your plugin to be published to npm. Once you're ready to publish your plugin publicly, run the following command in your plugin project: -### How to Consume the Hook? +```bash +npx medusa plugin:build +``` -To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content: +The command will compile an output in the `.medusa/server` directory. -```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights} -import { myWorkflow } from "../my-workflow" +You can now publish the plugin to npm using the [NPM CLI tool](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Run the following command to publish the plugin to npm: -myWorkflow.hooks.productCreated( - async ({ productId }, { container }) => { - // TODO perform an action - } -) +```bash +npm publish ``` -The hook is available on the workflow's `hooks` property using its name `productCreated`. +If you haven't logged in before with your NPM account, you'll be asked to log in first. Then, your package is published publicly to be used in any Medusa application. -You invoke the hook, passing a step function (the hook handler) as a parameter. +### Install Public Plugin in Medusa Application +You install a plugin that's published publicly using your package manager. For example: -# Compensation Function +```bash npm2yarn +npm install @myorg/plugin-name +``` -In this chapter, you'll learn what a compensation function is and how to add it to a step. +Where `@myorg/plugin-name` is the name of your plugin as published on NPM. -## What is a Compensation Function +Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). -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. +## Update a Published Plugin -By using compensation functions, you provide a mechanism that guarantees data consistency in your application and across systems. +To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md). -*** +If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. -## How to add a Compensation Function? +First, run the following command to change the version of the plugin: -A compensation function is passed as a second parameter to the `createStep` function. +```bash +npm version +``` -For example, create the file `src/workflows/hello-world.ts` with the following content: +Where `` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. Refer to the [npm version documentation](https://docs.npmjs.com/cli/v10/commands/npm-version) for more information. -```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" +Then, re-run the same commands for publishing a plugin: -const step1 = createStep( - "step-1", - async () => { - const message = `Hello from step one!` +```bash +npx medusa plugin:build +npm publish +``` - console.log(message) +This will publish an updated version of your plugin under a new version. - 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. +# Add Columns to a Link Table -*** +In this chapter, you'll learn how to add custom columns to a link definition's table and manage them. -## Test the Compensation Function +## Link Table's Default Columns -Create a step in the same `src/workflows/hello-world.ts` file that throws an error: +When you define a link between two data models, Medusa creates a link table in the database to store the IDs of the linked records. You can learn more about the created table in the [Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). -```ts title="src/workflows/hello-world.ts" -const step2 = createStep( - "step-2", - async () => { - throw new Error("Throwing an error...") - } -) -``` +In various cases, you might need to store additional data in the link table. For example, if you define a link between a `product` and a `post`, you might want to store the publish date of the product's post in the link table. -Then, create a workflow that uses the steps: +In those cases, you can add a custom column to a link's table in the link definition. You can later set that column whenever you create or update a link between the linked records. -```ts title="src/workflows/hello-world.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -// other imports... +*** -// steps... +## How to Add Custom Columns to a Link's Table? -const myWorkflow = createWorkflow( - "hello-world", - function (input) { - const str1 = step1() - step2() +The `defineLink` function used to define a link accepts a third parameter, which is an object of options. - return new WorkflowResponse({ - message: str1, - }) -}) +To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property: -export default myWorkflow +```ts highlights={linkHighlights} +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.blog, + { + database: { + extraColumns: { + metadata: { + type: "json", + }, + }, + }, + } +) ``` -Finally, execute the workflow from an API route: +This adds to the table created for the link between `product` and `blog` a `metadata` column of type `json`. -```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" +### Database Options -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await myWorkflow(req.scope) - .run() +The `database` property defines configuration for the table created in the database. - res.send(result) -} -``` +Its `extraColumns` property defines custom columns to create in the link's table. -Run the Medusa application and send a `GET` request to `/workflow`: +`extraColumns`'s value is an object whose keys are the names of the columns, and values are the column's configurations as an object. -```bash -curl http://localhost:9000/workflow -``` +### Column Configurations -In the console, you'll see: +The column's configurations object accepts the following properties: -- `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. +- `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. *** -## Pass Input to Compensation Function - -If a step creates a record, the compensation function must receive the ID of the record to remove it. +## Set Custom Column when Creating Link -To pass input to the compensation function, pass a second parameter in the `StepResponse` returned by the step. +The object you pass to 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: -```ts highlights={inputHighlights} -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" +Learn more about Link, how to resolve it, and its methods in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md). -const step1 = createStep( - "step-1", - async () => { - return new StepResponse( - `Hello from step one!`, - { message: "Oops! Rolling back my changes..." } - ) +```ts +await link.create({ + [Modules.PRODUCT]: { + product_id: "123", }, - async ({ message }) => { - console.log(message) - } -) + [BLOG_MODULE]: { + post_id: "321", + }, + data: { + metadata: { + test: true, + }, + }, +}) ``` -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 +## Retrieve Custom Column with Link -The compensation function receives an object second parameter. The object has a `container` property that you use to resolve resources from the Medusa container. +To retrieve linked records with their custom columns, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). A module link's definition, exported by a file under `src/links`, has a special `entryPoint` property. Use this property when specifying the `entity` property in Query's `graph` method. For example: -```ts -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +```ts highlights={retrieveHighlights} +import productPostLink from "../links/product-post" -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) - } -) +const { data } = await query.graph({ + entity: productPostLink.entryPoint, + fields: ["metadata", "product.*", "post.*"], + filters: { + product_id: "prod_123", + }, +}) ``` -In this example, you use the `container` property in the second object parameter of the compensation function to resolve the logger. +This retrieves the product of id `prod_123` and its linked `post` records. -You then use the logger to log a message. +In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link. *** -## Handle Errors in Loops +## Update Custom Column's Value -This feature is only available after [Medusa v2.0.5](https://github.com/medusajs/medusa/releases/tag/v2.0.5). +Link's `create` method updates a link's data if the link between the specified records already exists. -Consider you have a module that integrates a third-party ERP system, and you're creating a workflow that deletes items in that ERP. You may have the following step: +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 -// other imports... -import { promiseAll } from "@medusajs/framework/utils" +await link.create({ + [Modules.PRODUCT]: { + product_id: "123", + }, + [BLOG_MODULE]: { + post_id: "321", + }, + data: { + metadata: { + test: false, + }, + }, +}) +``` -type StepInput = { - ids: string[] -} -const step1 = createStep( - "step-1", - async ({ ids }: StepInput, { container }) => { - const erpModuleService = container.resolve( - ERP_MODULE - ) - const prevData: unknown[] = [] +# Module Link Direction - await promiseAll( - ids.map(async (id) => { - const data = await erpModuleService.retrieve(id) +In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case. - await erpModuleService.delete(id) +## Link Direction - prevData.push(id) - }) - ) +The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`. - return new StepResponse(ids, prevData) - } +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 ) ``` -In the step, you loop over the IDs to retrieve the item's data, store them in a `prevData` variable, then delete them using the ERP Module's service. You then pass the `prevData` variable to the compensation function. +Whereas the following defines a link from the Product Module's `product` data model to the `helloModuleService`'s `myCustom` data model: -However, if an error occurs in the loop, the `prevData` variable won't be passed to the compensation function as the execution never reached the return statement. +```ts +export default defineLink( + ProductModule.linkable.product, + HelloModule.linkable.myCustom +) +``` -To handle errors in the loop so that the compensation function receives the last version of `prevData` before the error occurred, you wrap the loop in a try-catch block. Then, in the catch block, you invoke and return the `StepResponse.permanentFailure` function: +The above links are two different links that serve different purposes. -```ts highlights={highlights} -try { - await promiseAll( - ids.map(async (id) => { - const data = await erpModuleService.retrieve(id) +*** - await erpModuleService.delete(id) +## Which Link Direction to Use? - prevData.push(id) - }) - ) -} catch (e) { - return StepResponse.permanentFailure( - `An error occurred: ${e}`, - prevData - ) -} +### 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, consider you want to add a `subtitle` custom field to the `product` data model. To do that, you define a `Subtitle` data model in your module, then define a link from the `Product` data model to it: + +```ts +export default defineLink( + ProductModule.linkable.product, + HelloModule.linkable.subtitle +) ``` -The `StepResponse.permanentFailure` fails the step and its workflow, triggering current and previous steps' compensation functions. The `permanentFailure` function accepts as a first parameter the error message, which is saved in the workflow's error details, and as a second parameter the data to pass to the compensation function. +### Associate Data Models -So, if an error occurs during the loop, the compensation function will still receive the `prevData` variable to undo the changes made before the step failed. +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, consider you have `Post` data model representing a blog post, and you want to associate a blog post with a product. To do that, define a link from the `Post` data model to `Product`: -# Conditions in Workflows with When-Then +```ts +export default defineLink( + HelloModule.linkable.post, + ProductModule.linkable.product +) +``` -In this chapter, you'll learn how to execute an action based on a condition in a workflow using when-then from the Workflows SDK. -## Why If-Conditions Aren't Allowed in Workflows? +# Query -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. +In this chapter, you’ll learn about Query and how to use it to fetch data from modules. -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. +## What is Query? -Instead, use when-then from the Workflows SDK. It allows you to perform steps in a workflow only if a condition that you specify is satisfied. +Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key. -Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code. +In all resources that can access the [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s commerce modules. *** -## How to use When-Then? +## Query Example -The Workflows SDK provides a `when` function that is used to check whether a condition is true. You chain a `then` function to `when` that specifies the steps to execute if the condition in `when` is satisfied. +For example, create the route `src/api/query/route.ts` with the following content: -For example: +```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" -```ts highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - when, -} from "@medusajs/framework/workflows-sdk" -// step imports... +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) -const workflow = createWorkflow( - "workflow", - function (input: { - is_active: boolean - }) { + const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + }) - const result = when( - input, - (input) => { - return input.is_active - } - ).then(() => { - const stepResult = isActiveStep() - return stepResult - }) + res.json({ my_customs: myCustoms }) +} +``` - // executed without condition - const anotherStepResult = anotherStep(result) +In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key. - return new WorkflowResponse( - anotherStepResult - ) - } -) -``` +Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties: -In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`. +- `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. -### When Parameters +The method returns an object that has a `data` property, which holds an array of the retrieved data. For example: -`when` accepts the following parameters: +```json title="Returned Data" +{ + "data": [ + { + "id": "123", + "name": "test" + } + ] +} +``` -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 +## Querying the Graph -To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. +When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates. -The callback function is only executed if `when`'s second parameter function returns a `true` value. +This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them. *** -## Implementing If-Else with When-Then +## Retrieve Linked Records -when-then doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow. +Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`. For example: -```ts highlights={ifElseHighlights} -const workflow = createWorkflow( - "workflow", - function (input: { - is_active: boolean - }) { - - const isActiveResult = when( - input, - (input) => { - return input.is_active - } - ).then(() => { - return isActiveStep() - }) - - const notIsActiveResult = when( - input, - (input) => { - return !input.is_active - } - ).then(() => { - return notIsActiveStep() - }) - - // ... - } -) +```ts highlights={[["6"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: [ + "id", + "name", + "product.*", + ], +}) ``` -In the above workflow, you use two `when-then` blocks. The first one performs a step if `input.is_active` is `true`, and the second performs a step if `input.is_active` is `false`, acting as an else condition. +`.*` 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`. -*** +### Retrieve List Link Records -## Specify Name for When-Then +If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`. -Internally, `when-then` blocks have a unique name similar to a step. When you return a step's result in a `when-then` block, the block's name is derived from the step's name. For example: +For example: -```ts -const isActiveResult = when( - input, - (input) => { - return input.is_active - } -).then(() => { - return isActiveStep() +```ts highlights={[["6"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: [ + "id", + "name", + "products.*", + ], }) ``` -This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name. +### Apply Filters and Pagination on Linked Records -However, if you need to return in your `when-then` block something other than a step's result, you need to specify a unique step name for that block. Otherwise, Medusa will generate a random name for it which can cause unexpected errors in production. +Consider you want to apply filters or pagination configurations on the product(s) linked to `my_custom`. To do that, you must query the module link's table instead. -You pass a name for `when-then` as a first parameter of `when`, whose signature can accept three parameters in this case. For example: +As mentioned in the [Module Link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) documentation, Medusa creates a table for your module link. So, not only can you retrieve linked records, but you can also retrieve the records in a module link's table. -```ts highlights={nameHighlights} -const { isActive } = when( - "check-is-active", - input, - (input) => { - return input.is_active - } -).then(() => { - const isActive = isActiveStep() +A module link's definition, exported by a file under `src/links`, has a special `entryPoint` property. Use this property when specifying the `entity` property in Query's `graph` method. - return { - isActive, - } +For example: + +```ts highlights={queryLinkTableHighlights} +import productCustomLink from "../../../links/product-custom" + +// ... + +const { data: productCustoms } = await query.graph({ + entity: productCustomLink.entryPoint, + fields: ["*", "product.*", "my_custom.*"], + pagination: { + take: 5, + skip: 0, + }, }) ``` -Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters: +In the object passed to the `graph` method: -1. A unique name to be assigned to the `when-then` block. -2. Either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. -3. A function that returns a boolean indicating whether to execute the action in `then`. +- You pass the `entryPoint` property of the link definition as the value for `entity`. So, Query will retrieve records from the module link's table. +- You pass three items to the `field` property: + - `*` to retrieve the link table's fields. This is useful if the link table has [custom columns](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns/index.html.md). + - `product.*` to retrieve the fields of a product record linked to a `MyCustom` record. + - `my_custom.*` to retrieve the fields of a `MyCustom` record linked to a product record. -The second and third parameters are the same as the parameters you previously passed to `when`. +You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination). +The returned `data` is similar to the following: -# Workflow Constraints +```json title="Example Result" +[{ + "id": "123", + "product_id": "prod_123", + "my_custom_id": "123", + "product": { + "id": "prod_123", + // other product fields... + }, + "my_custom": { + "id": "123", + // other my_custom fields... + } +}] +``` -This chapter lists constraints of defining a workflow or its steps. +*** -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. +## Apply Filters -This creates restrictions related to variable manipulations, using if-conditions, and other constraints. This chapter lists these constraints and provides their alternatives. +```ts highlights={[["6"], ["7"], ["8"], ["9"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + filters: { + id: [ + "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX", + "mc_01HWSVWK3KYHKQEE6QGS2JC3FX", + ], + }, +}) +``` -## Workflow Constraints +The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records. -### No Async Functions +In the example above, you filter the `my_custom` records by multiple IDs. -The function passed to `createWorkflow` can’t be an async function: +Filters don't apply on fields of linked data models from other modules. -```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) { - // ... +## 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, + }, }) ``` -### No Direct Variable Manipulation - -You can’t directly manipulate variables within the workflow's constructor function. +The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records. -Learn more about why you can't manipulate variables [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) +To paginate the returned records, pass the following properties to `pagination`: -Instead, use `transform` from the Workflows SDK: +- `skip`: (required to apply pagination) The number of records to skip before fetching the results. +- `take`: The number of records to fetch. -```ts highlights={highlights} -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const str1 = step1(input) - const str2 = step2(input) +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: - return new WorkflowResponse({ - message: `${str1}${str2}`, - }) -}) +- skip: (\`number\`) The number of records skipped. +- take: (\`number\`) The number of records requested to fetch. +- count: (\`number\`) The total number of records. -// Do -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const str1 = step1(input) - const str2 = step2(input) +### Sort Records - const result = transform( - { - str1, - str2, +```ts highlights={[["5"], ["6"], ["7"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + pagination: { + order: { + name: "DESC", }, - (input) => ({ - message: `${input.str1}${input.str2}`, - }) - ) - - return new WorkflowResponse(result) + }, }) ``` -### Create Dates in transform - -When you use `new Date()` in a workflow's constructor function, the date is evaluated when Medusa creates the internal representation of the workflow, not during execution. +Sorting doesn't work on fields of linked data models from other modules. -Instead, create the date using `transform`. +To sort returned records, pass an `order` property to `pagination`. -Learn more about how Medusa creates an internal representation of a workflow [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). +The `order` property is an object whose keys are property names, and values are either: -For example: +- `ASC` to sort records by that property in ascending order. +- `DESC` to sort records by that property in descending order. -```ts highlights={dateHighlights} -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const today = new Date() +*** - return new WorkflowResponse({ - today, - }) -}) +## Request Query Configurations -// Do -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const today = transform({}, () => new Date()) +For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that: - return new WorkflowResponse({ - today, - }) -}) -``` +- Validates accepted query parameters, as explained in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). +- Parses configurations that are received as query parameters to be passed to Query. -### No If Conditions +Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request. -You can't use if-conditions in a workflow. +### Step 1: Add Middleware -Learn more about why you can't use if-conditions [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows/index.html.md) +The first step is to use the `validateAndTransformQuery` middleware on the `GET` route. You add the middleware in `src/api/middlewares.ts`: -Instead, use when-then from the Workflows SDK: +```ts title="src/api/middlewares.ts" +import { + validateAndTransformQuery, + defineMiddlewares, +} from "@medusajs/framework/http" +import { createFindParams } from "@medusajs/medusa/api/utils/validators" -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - if (input.is_active) { - // perform an action - } -}) +export const GetCustomSchema = createFindParams() -// 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 - }) +export default defineMiddlewares({ + routes: [ + { + matcher: "/customs", + method: "GET", + middlewares: [ + validateAndTransformQuery( + GetCustomSchema, + { + defaults: [ + "id", + "name", + "products.*", + ], + isList: true, + } + ), + ], + }, + ], }) ``` -You can also pair multiple `when-then` blocks to implement an `if-else` condition as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). - -### No Conditional Operators - -You can't use conditional operators in a workflow, such as `??` or `||`. - -Learn more about why you can't use conditional operators [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows/index.html.md) +The `validateAndTransformQuery` accepts two parameters: -Instead, use `transform` to store the desired value in a variable. +1. A Zod validation schema for the query parameters, which you can learn more about in the [API Route Validation documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). Medusa has a `createFindParams` utility that generates a Zod schema that accepts four query parameters: + 1. `fields`: The fields and relations to retrieve in the returned resources. + 2. `offset`: The number of items to skip before retrieving the returned items. + 3. `limit`: The maximum number of items to return. + 4. `order`: The fields to order the returned items by in ascending or descending order. +2. A Query configuration object. It accepts the following properties: + 1. `defaults`: An array of default fields and relations to retrieve in each resource. + 2. `isList`: A boolean indicating whether a list of items are returned in the response. + 3. `allowed`: An array of fields and relations allowed to be passed in the `fields` query parameter. + 4. `defaultLimit`: A number indicating the default limit to use if no limit is provided. By default, it's `50`. -### Logical Or (||) Alternative +### Step 2: Use Configurations in API Route -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = input.message || "Hello" -}) +After applying this middleware, your API route now accepts the `fields`, `offset`, `limit`, and `order` query parameters mentioned above. -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" +The middleware transforms these parameters to configurations that you can pass to Query in your API route handler. These configurations are stored in the `queryConfig` parameter of the `MedusaRequest` object. -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = transform( - { - input, - }, - (data) => data.input.message || "hello" - ) -}) -``` +As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), `remoteQueryConfig` has been depercated in favor of `queryConfig`. Their usage is still the same, only the property name has changed. -### Nullish Coalescing (??) Alternative +For example, Create the file `src/api/customs/route.ts` with the following content: -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = input.message ?? "Hello" -}) +```ts title="src/api/customs/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = transform( - { - input, - }, - (data) => data.input.message ?? "hello" - ) -}) + const { data: myCustoms } = await query.graph({ + entity: "my_custom", + ...req.queryConfig, + }) + + res.json({ my_customs: myCustoms }) +} ``` -### Double Not (!!) Alternative +This adds a `GET` API route at `/customs`, which is the API route you added the middleware for. -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - step1({ - isActive: !!input.is_active, - }) -}) +In the API route, you pass `req.queryConfig` to `query.graph`. `queryConfig` has properties like `fields` and `pagination` to configure the query based on the default values you specified in the middleware, and the query parameters passed in the request. -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" +### Test it Out -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const isActive = transform( - { - input, - }, - (data) => !!data.input.is_active - ) - - step1({ - isActive, - }) -}) +To test it out, start your Medusa application and send a `GET` request to the `/customs` API route. A list of records are retrieved with the specified fields in the middleware. + +```json title="Returned Data" +{ + "my_customs": [ + { + "id": "123", + "name": "test" + } + ] +} ``` -### Ternary Alternative +Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result. -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - step1({ - message: input.is_active ? "active" : "inactive", - }) -}) +Learn more about [specifing fields and relations](https://docs.medusajs.com/api/store#select-fields-and-relations) and [pagination](https://docs.medusajs.com/api/store#pagination) in the API reference. -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = transform( - { - input, - }, - (data) => { - return data.input.is_active ? "active" : "inactive" - } - ) - - step1({ - message, - }) -}) -``` +# Link -### Optional Chaining (?.) Alternative +In this chapter, you’ll learn what Link is and how to use it to manage links. -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - step1({ - name: input.customer?.name, - }) -}) +As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), Remote Link has been deprecated in favor of Link. They have the same usage, so you only need to change the key used to resolve the tool from the Medusa container as explained below. -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" +## What is Link? -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const name = transform( - { - input, - }, - (data) => data.input.customer?.name - ) +Link is a class with utility methods to manage links between data models. It’s registered in the Medusa container under the `link` registration name. + +For example: + +```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" + +export async function POST( + req: MedusaRequest, + res: MedusaResponse +): Promise { + const link = req.scope.resolve( + ContainerRegistrationKeys.LINK + ) - step1({ - name, - }) -}) + // ... +} ``` -*** +You can use its methods to manage links, such as create or delete links. -## Step Constraints +*** -### Returned Values +## Create Link -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. +To create a link between records of two data models, use the `create` method of Link. -Values of other types, such as Maps, aren't allowed. +For example: ```ts -// Don't -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" -const step1 = createStep( - "step-1", - (input, { container }) => { - const myMap = new Map() +// ... - // ... +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + "helloModuleService": { + my_custom_id: "mc_123", + }, +}) +``` - return new StepResponse({ - myMap, - }) - } -) +The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules. -// Do -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" +The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition. -const step1 = createStep( - "step-1", - (input, { container }) => { - const myObj: Record = {} +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. - return new StepResponse({ - myObj, - }) - } -) -``` - - -# Execute Another Workflow - -In this chapter, you'll learn how to execute a workflow in another. +*** -## Execute in a Workflow +## Dismiss Link -To execute a workflow in another, use the `runAsStep` method that every workflow has. +To remove a link between records of two data models, use the `dismiss` method of Link. For example: -```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports" -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" +```ts +import { Modules } from "@medusajs/framework/utils" -const workflow = createWorkflow( - "hello-world", - async (input) => { - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - // ... - ], - }, - }) +// ... - // ... - } -) +await link.dismiss({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + "helloModuleService": { + my_custom_id: "mc_123", + }, +}) ``` -Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. +The `dismiss` method accepts the same parameter type as the [create method](#create-link). -The object has an `input` property to pass input to the workflow. +The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition. *** -## Preparing Input Data - -If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. +## Cascade Delete Linked Records -Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). +If a record is deleted, use the `delete` method of Link to delete all linked records. For example: -```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 -} +```ts +import { Modules } from "@medusajs/framework/utils" -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, - }, - }) +await productModuleService.deleteVariants([variant.id]) - // ... - } -) +await link.delete({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, +}) ``` -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`. +This deletes all records linked to the deleted product. *** -## Run Workflow Conditionally - -To run a workflow in another based on a condition, use when-then from the Workflows SDK. +## Restore Linked Records -Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). +If a record that was previously soft-deleted is now restored, use the `restore` method of Link to restore all linked records. For example: -```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 when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. +```ts +import { Modules } from "@medusajs/framework/utils" +// ... -# Long-Running Workflows +await productModuleService.restoreProducts(["prod_123"]) -In this chapter, you’ll learn what a long-running workflow is and how to configure it. +await link.restore({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, +}) +``` -## What is a Long-Running Workflow? -When you execute a workflow, you wait until the workflow finishes execution to receive the output. +# Architectural Modules -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. +In this chapter, you’ll learn about architectural modules. -### Why use Long-Running Workflows? +## What is an Architectural Module? -Long-running workflows are useful if: +An architectural module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. -- 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. +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. *** -## Configure Long-Running Workflows +## Architectural Module Types -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. +There are different architectural module types including: -For example, consider the following workflow and steps: +![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/architectural-modules_bj9bb9.jpg) -```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" +- 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. -const step1 = createStep("step-1", async () => { - return new StepResponse({}) -}) +*** -const step2 = createStep( - { - name: "step-2", - async: true, - }, - async () => { - console.log("Waiting to be successful...") - } -) +## Architectural Modules List -const step3 = createStep("step-3", async () => { - return new StepResponse("Finished three steps") -}) +Refer to the [Architectural Modules reference](https://docs.medusajs.com/resources/architectural-modules/index.html.md) for a list of Medusa’s architectural modules, available modules to install, and how to create an architectural module. -const myWorkflow = createWorkflow( - "hello-world", - function () { - step1() - step2() - const message = step3() - return new WorkflowResponse({ - message, - }) -}) +# Query Context -export default myWorkflow -``` +In this chapter, you'll learn how to pass contexts when retrieving data with [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). -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. +## What is Query Context? -So, when you execute the `hello-world` workflow, it continues its execution in the background once it reaches the second step. +Query context is a way to pass additional information when retrieving data with Query. This data can be useful when applying custom transformations to the retrieved data based on the current context. -A workflow is also considered long-running if one of its steps has their `retryInterval` option set as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md). +For example, consider you have a Blog Module with posts and authors. You can accept the user's language as a context and return the posts in the user's language. Another example is how Medusa uses Query Context to [retrieve product variants' prices based on the customer's currency](https://docs.medusajs.com/resources/commerce-modules/product/guides/price/index.html.md). *** -## 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. +## How to Use Query Context -### Retrieve Transaction ID +The `query.graph` method accepts an optional `context` parameter that can be used to pass additional context either to the data model you're retrieving (for example, `post`), or its related and linked models (for example, `author`). -Before changing the status of a workflow execution's async step, you must have the execution's transaction ID. +You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument. -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: +For example, to retrieve posts using Query while passing the user's language as a context: ```ts -const { transaction } = await myWorkflow(req.scope) - .run() - -// use transaction.transactionId later +const { data } = await query.graph({ + entity: "post", + fields: ["*"], + context: QueryContext({ + lang: "es", + }), +}) ``` -### 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. +In this example, you pass in the context a `lang` property whose value is `es`. -For example, consider the following step: +Then, to handle the context while retrieving records of the data model, in the associated module's service you override the generated `list` method of the data model. -```ts highlights={successStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - Modules, - TransactionHandlerType, -} from "@medusajs/framework/utils" -import { - StepResponse, - createStep, -} from "@medusajs/framework/workflows-sdk" +For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context: -type SetStepSuccessStepInput = { - transactionId: string -}; +```ts highlights={highlights2} +import { MedusaContext, MedusaService } from "@medusajs/framework/utils" +import { Context, FindConfig } from "@medusajs/framework/types" +import Post from "./models/post" +import Author from "./models/author" -export const setStepSuccessStep = createStep( - "set-step-success-step", - async function ( - { transactionId }: SetStepSuccessStepInput, - { container } +class BlogModuleService extends MedusaService({ + Post, + Author, +}){ + // @ts-ignore + async listPosts( + filters?: any, + config?: FindConfig | undefined, + @MedusaContext() sharedContext?: Context | undefined ) { - 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: + const context = filters.context ?? {} + delete filters.context -- idempotencyKey: (\`object\`) The details of the workflow execution. + let posts = await super.listPosts(filters, config, sharedContext) - - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. + if (context.lang === "es") { + posts = posts.map((post) => { + return { + ...post, + title: post.title + " en español", + } + }) + } - - transactionId: (\`string\`) The ID of the workflow execution's transaction. + return posts + } +} - - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step. +export default BlogModuleService +``` - - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow. -- stepResponse: (\`StepResponse\`) 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. -- options: (\`Record\\`) Options to pass to the step. +In the above example, you override the generated `listPosts` method. This method receives as a first parameter the filters passed to the query, but it also includes a `context` property that holds the context passed to the query. - - container: (\`MedusaContainer\`) An instance of the Medusa Container +You extract the context from `filters`, then retrieve the posts using the parent's `listPosts` method. After that, if the language is set in the context, you transform the titles of the posts. -### Change Step Status to Failed +All posts returned will now have their titles appended with "en español". -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`. +Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md). -After changing the async step's status to failed, the workflow execution fails and the compensation functions of previous steps are executed. +### Using Pagination with Query -For example: +If you pass pagination fields to `query.graph`, you must also override the `listAndCount` method in the service. -```ts highlights={failureStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - Modules, - TransactionHandlerType, -} from "@medusajs/framework/utils" -import { - StepResponse, - createStep, -} from "@medusajs/framework/workflows-sdk" +For example, following along with the previous example, you must override the `listAndCountPosts` method of the Blog Module's service: -type SetStepFailureStepInput = { - transactionId: string -}; +```ts +import { MedusaContext, MedusaService } from "@medusajs/framework/utils" +import { Context, FindConfig } from "@medusajs/framework/types" +import Post from "./models/post" +import Author from "./models/author" -export const setStepFailureStep = createStep( - "set-step-success-step", - async function ( - { transactionId }: SetStepFailureStepInput, - { container } +class BlogModuleService extends MedusaService({ + Post, + Author, +}){ + // @ts-ignore + async listAndCountPosts( + filters?: any, + config?: FindConfig | undefined, + @MedusaContext() sharedContext?: Context | undefined ) { - const workflowEngineService = container.resolve( - Modules.WORKFLOW_ENGINE + const context = filters.context ?? {} + delete filters.context + + const result = await super.listAndCountPosts( + filters, + config, + sharedContext ) - await workflowEngineService.setStepFailure({ - idempotencyKey: { - action: TransactionHandlerType.INVOKE, - transactionId, - stepId: "step-2", - workflowId: "hello-world", - }, - stepResponse: new StepResponse("Failed!"), - options: { - container, - }, - }) + if (context.lang === "es") { + result.posts = posts.map((post) => { + return { + ...post, + title: post.title + " en español", + } + }) + } + + return result } -) +} + +export default BlogModuleService ``` -You use this step in another workflow that changes the status of an async step in a long-running workflow's execution to failed. +Now, the `listAndCountPosts` method will handle the context passed to `query.graph` when you pass pagination fields. You can also move the logic to transform the posts' titles to a separate method and call it from both `listPosts` and `listAndCountPosts`. *** -## Access Long-Running Workflow Status and Result +## Passing Query Context to Related Data Models -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. +If you're retrieving a data model and you want to pass context to its associated model in the same module, you can pass them as part of `QueryContext`'s parameter, then handle them in the same `list` method. -To retrieve the workflow execution's details at a later point, you must enable [storing the workflow's executions](https://docs.medusajs.com/learn/fundamentals/workflows/store-executions/index.html.md). +For linked data models, check out the [next section](#passing-query-context-to-linked-data-models). -For example: +For example, to pass a context for the post's authors: -```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" +```ts highlights={highlights3} +const { data } = await query.graph({ + entity: "post", + fields: ["*"], + context: QueryContext({ + lang: "es", + author: QueryContext({ + lang: "es", + }), + }), +}) +``` -export async function GET(req: MedusaRequest, res: MedusaResponse) { - const { transaction, result } = await myWorkflow(req.scope).run() +Then, in the `listPosts` method, you can handle the context for the post's authors: - const workflowEngineService = req.scope.resolve< - IWorkflowEngineService - >( - Modules.WORKFLOW_ENGINE - ) +```ts highlights={highlights4} +import { MedusaContext, MedusaService } from "@medusajs/framework/utils" +import { Context, FindConfig } from "@medusajs/framework/types" +import Post from "./models/post" +import Author from "./models/author" - const subscriptionOptions = { - workflowId: "hello-world", - transactionId: transaction.transactionId, - subscriberId: "hello-world-subscriber", - } +class BlogModuleService extends MedusaService({ + Post, + Author, +}){ + // @ts-ignore + async listPosts( + filters?: any, + config?: FindConfig | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context - 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) - } - }, - }) + let posts = await super.listPosts(filters, config, sharedContext) - res.send(result) -} -``` + const isPostLangEs = context.lang === "es" + const isAuthorLangEs = context.author?.lang === "es" -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. + if (isPostLangEs || isAuthorLangEs) { + posts = posts.map((post) => { + return { + ...post, + title: isPostLangEs ? post.title + " en español" : post.title, + author: { + ...post.author, + name: isAuthorLangEs ? post.author.name + " en español" : post.author.name, + }, + } + }) + } -### subscribe Method + return posts + } +} -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: +export default BlogModuleService +``` -- workflowId: (\`string\`) The name of the workflow. -- transactionId: (\`string\`) The ID of the workflow exection's transaction. The transaction's details are returned in the response of the workflow execution. -- subscriberId: (\`string\`) The ID of the subscriber. -- subscriber: (\`(data: \{ eventType: string, result?: any }) => Promise\\`) 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. +The context in `filters` will also have the context for `author`, which you can use to make transformations to the post's authors. -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 +## Passing Query Context to Linked Data Models -You can unsubscribe from the workflow using the workflow engine's `unsubscribe` method, which requires the same object parameter as the `subscribe` method. +If you're retrieving a data model and you want to pass context to a linked model in a different module, pass to the `context` property an object instead, where its keys are the linked model's name and the values are the context for that linked model. -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](https://docs.medusajs.com/resources/recipes/marketplace/examples/restaurant-delivery/index.html.md). +For example, consider the Product Module's `Product` data model is linked to the Blog Module's `Post` data model. You can pass context to the `Post` data model while retrieving products like so: -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. +```ts highlights={highlights5} +const { data } = await query.graph({ + entity: "product", + fields: ["*", "post.*"], + context: { + post: QueryContext({ + lang: "es", + }), + }, +}) +``` +In this example, you retrieve products and their associated posts. You also pass a context for `post`, indicating the customer's language. -# Multiple Step Usage in Workflow +To handle the context, you override the generated `listPosts` method of the Blog Module as explained [previously](#how-to-use-query-context). -In this chapter, you'll learn how to use a step multiple times in a workflow. -## Problem Reusing a Step in a Workflow +# Commerce Modules -In some cases, you may need to use a step multiple times in the same workflow. +In this chapter, you'll learn about Medusa's commerce modules. -The most common example is using the `useQueryGraphStep` multiple times in a workflow to retrieve multiple unrelated data, such as customers and products. +## What is a Commerce Module? -Each workflow step must have a unique ID, which is the ID passed as a first parameter when creating the step: +Commerce modules are built-in [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) of Medusa that provide core commerce logic specific to domains like Products, Orders, Customers, Fulfillment, and much more. -```ts -const useQueryGraphStep = createStep( - "use-query-graph" - // ... -) -``` +Medusa's commerce modules are used to form Medusa's default [workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) and [APIs](https://docs.medusajs.com/api/store). For example, when you call the add to cart endpoint. the add to cart workflow runs which uses the Product Module to check if the product exists, the Inventory Module to ensure the product is available in the inventory, and the Cart Module to finally add the product to the cart. -This causes an error when you use the same step multiple times in a workflow, as it's registered in the workflow as two steps having the same ID: +You'll find the details and steps of the add-to-cart workflow in [this workflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/addToCartWorkflow/index.html.md) -```ts -const helloWorkflow = createWorkflow( - "hello", - () => { - const { data: products } = useQueryGraphStep({ - entity: "product", - fields: ["id"], - }) +The core commerce logic contained in Commerce Modules is also available directly when you are building customizations. This granular access to commerce functionality is unique and expands what's possible to build with Medusa drastically. - // ERROR OCCURS HERE: A STEP HAS THE SAME ID AS ANOTHER IN THE WORKFLOW - const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: ["id"], - }) - } -) -``` +### List of Medusa's Commerce Modules -The next section explains how to fix this issue to use the same step multiple times in a workflow. +Refer to [this reference](https://docs.medusajs.com/resources/commerce-modules/index.html.md) for a full list of commerce modules in Medusa. *** -## How to Use a Step Multiple Times in a Workflow? - -When you execute a step in a workflow, you can chain a `config` method to it to change the step's config. +## Use Commerce Modules in Custom Flows -Use the `config` method to change a step's ID for a single execution. +Similar to your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), the Medusa application registers a commerce module's service in the [container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md). So, you can resolve it in your custom flows. This is useful as you build unique requirements extending core commerce features. -So, this is the correct way to write the example above: +For example, consider you have a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) (a special function that performs a task in a series of steps with rollback mechanism) that needs a step to retrieve the total number of products. You can create a step in the workflow that resolves the Product Module's service from the container to use its methods: ```ts highlights={highlights} -const helloWorkflow = createWorkflow( - "hello", - () => { - const { data: products } = useQueryGraphStep({ - entity: "product", - fields: ["id"], - }) +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" - // ✓ No error occurs, the step has a different ID. - const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: ["id"], - }).config({ name: "fetch-customers" }) +export const countProductsStep = createStep( + "count-products", + async ({ }, { container }) => { + const productModuleService = container.resolve("product") + + const [,count] = await productModuleService.listAndCountProducts() + + return new StepResponse(count) } ) ``` -The `config` method accepts an object with a `name` property. Its value is a new ID of the step to use for this execution only. - -The first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`. +Your workflow can use services of both custom and commerce modules, supporting you in building custom flows without having to re-build core commerce features. -# Run Workflow Steps in Parallel +# Perform Database Operations in a Service -In this chapter, you’ll learn how to run workflow steps in parallel. +In this chapter, you'll learn how to perform database operations in a module's service. -## parallelize Utility Function +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](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) instead. -If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK. +## Run Queries -The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step. +[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. -For example: +Medusa provides an `InjectManager` decorator from the Modules SDK that injects a service's method with a [forked entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager). -```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" +So, to run database queries in a service: -interface WorkflowInput { - title: string -} +1. Add the `InjectManager` decorator to the method. +2. Add as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator from the Modules SDK. This context holds database-related context, including the manager injected by `InjectManager` -const myWorkflow = createWorkflow( - "my-workflow", - (input: WorkflowInput) => { - const product = createProductStep(input) +For example, in your service, add the following methods: - const [prices, productSalesChannel] = parallelize( - createPricesStep(product), - attachProductToSalesChannelStep(product) - ) +```ts highlights={methodsHighlight} +// other imports... +import { + InjectManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { SqlEntityManager } from "@mikro-orm/knex" - const id = product.id - const refetchedProduct = getProductStep(product.id) +class HelloModuleService { + // ... - return new WorkflowResponse(refetchedProduct) - } -) + @InjectManager() + async getCount( + @MedusaContext() sharedContext?: Context + ): Promise { + return await sharedContext.manager.count("my_custom") + } + + @InjectManager() + async getCountSql( + @MedusaContext() sharedContext?: Context + ): Promise { + const data = await sharedContext.manager.execute( + "SELECT COUNT(*) as num FROM my_custom" + ) + + return parseInt(data[0].num) + } +} ``` -The `parallelize` function accepts the steps to run in parallel as a parameter. +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. -It returns an array of the steps' results in the same order they're passed to the `parallelize` function. +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/knex/class/EntityManager). -So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`. +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. +Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. -# Retry Failed Steps +*** -In this chapter, you’ll learn how to configure steps to allow retrial on failure. +## Execute Operations in Transactions -## Configure a Step’s Retrial +To wrap database operations in a transaction, you create two methods: -By default, when an error occurs in a step, the step and the workflow fail, and the execution stops. +1. A private or protected method that's wrapped in a transaction. To wrap it in a transaction, you use the `InjectTransactionManager` decorator from the Modules SDK. +2. A public method that calls the transactional method. You use on it the `InjectManager` decorator as explained in the previous section. -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. +Both methods must accept as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator from the Modules SDK. It holds database-related contexts passed through the Medusa application. For example: -```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +```ts highlights={opHighlights} import { - createStep, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - { - name: "step-1", - maxRetries: 2, - }, - async () => { - console.log("Executing step 1") + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" - throw new Error("Oops! Something happened.") - } -) +class HelloModuleService { + // ... + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + const transactionManager = sharedContext.transactionManager + await transactionManager.nativeUpdate( + "my_custom", + { + id: input.id, + }, + { + name: input.name, + } + ) -const myWorkflow = createWorkflow( - "hello-world", - function () { - const str1 = step1() + // retrieve again + const updatedRecord = await transactionManager.execute( + `SELECT * FROM my_custom WHERE id = '${input.id}'` + ) - return new WorkflowResponse({ - message: str1, - }) -}) + return updatedRecord + } -export default myWorkflow + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ) { + return await this.update_(input, sharedContext) + } +} ``` -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. +The `HelloModuleService` has two methods: -When you execute the above workflow, you’ll see the following result in the terminal: +- A protected `update_` that performs the database operations inside a transaction. +- A public `update` that executes the transactional protected method. -```bash -Executing step 1 -Executing step 1 -Executing step 1 -error: Oops! Something happened. -Error: Oops! Something happened. -``` +The shared context's `transactionManager` property holds the transactional entity manager (injected by `InjectTransactionManager`) that you use to perform database operations. -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. +Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. -*** +### Why Wrap a Transactional Method -## Step Retry Intervals +The variables in the transactional method (for example, `update_`) hold values that are uncommitted to the database. They're only committed once the method finishes execution. -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. +So, if in your method you perform database operations, then use their result to perform other actions, such as connecting to a third-party service, you'll be working with uncommitted data. -For example: +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. -```ts title="src/workflows/hello-world.ts" highlights={[["5"]]} -const step1 = createStep( - { - name: "step-1", - maxRetries: 2, - retryInterval: 2, // 2 seconds - }, - async () => { - // ... +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. + +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 + ) { + const newData = await this.update_(input, sharedContext) + + await sendNewDataToSystem(newData) + + return newData } -) +} ``` -### Interval Changes Workflow to Long-Running +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. -By setting `retryInterval` on a step, a workflow becomes a [long-running workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) that runs asynchronously in the background. So, you won't receive its result or errors immediately when you execute the workflow. +### Using Methods in Transactional Methods -Instead, you must subscribe to the workflow's execution using the Workflow Engine Module Service. Learn more about it in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md). +If your transactional method uses other methods that accept a Medusa context, pass the shared context to those methods. +For example: -# Store Workflow Executions +```ts +// other imports... +import { EntityManager } from "@mikro-orm/knex" -In this chapter, you'll learn how to store workflow executions in the database and access them later. +class HelloModuleService { + // ... + @InjectTransactionManager() + protected async anotherMethod( + @MedusaContext() sharedContext?: Context + ) { + // ... + } + + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + this.anotherMethod(sharedContext) + } +} +``` -## Workflow Execution Retention +You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context. -Medusa doesn't store your workflow's execution details by default. However, you can configure a workflow to keep its execution details stored in the database. +The `anotherMethod` now runs in the same transaction as the `update_` method. -This is useful for auditing and debugging purposes. When you store a workflow's execution, you can view details around its steps, their states and their output. You can also check whether the workflow or any of its steps failed. +*** -You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows. +## 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. -## How to Store Workflow's Executions? +The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more. -### Prerequisites +The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction. -- [Redis Workflow Engine must be installed and configured.](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/redis/index.html.md) +For example, resolve the `baseRepository` in your service's constructor: -`createWorkflow` from the Workflows SDK can accept an object as a first parameter to set the workflow's configuration. To enable storing a workflow's executions: +### Extending Service Factory -- Enable the `store` option. If your workflow is a [Long-Running Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md), this option is enabled by default. -- Set the `retentionTime` option to the number of seconds that the workflow execution should be stored in the database. +```ts highlights={[["14"]]} +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" +import { DAL } from "@medusajs/framework/types" -For example: +type InjectedDependencies = { + baseRepository: DAL.RepositoryService +} -```ts highlights={highlights} -import { createStep, createWorkflow } from "@medusajs/framework/workflows-sdk" +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + protected baseRepository_: DAL.RepositoryService -const step1 = createStep( - { - name: "step-1", - }, - async () => { - console.log("Hello from step 1") + constructor({ baseRepository }: InjectedDependencies) { + super(...arguments) + this.baseRepository_ = baseRepository } -) +} -export const helloWorkflow = createWorkflow( - { - name: "hello-workflow", - retentionTime: 99999, - store: true, - }, - () => { - step1() - } -) +export default HelloModuleService ``` -Whenever you execute the `helloWorkflow` now, its execution details will be stored in the database. +### Without Service Factory -*** +```ts highlights={[["10"]]} +import { DAL } from "@medusajs/framework/types" -## Retrieve Workflow Executions +type InjectedDependencies = { + baseRepository: DAL.RepositoryService +} -You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows. +class HelloModuleService { + protected baseRepository_: DAL.RepositoryService -When you execute a workflow, the returned object has a `transaction` property containing the workflow execution's transaction details: + constructor({ baseRepository }: InjectedDependencies) { + this.baseRepository_ = baseRepository + } +} -```ts -const { transaction } = await helloWorkflow(container).run() +export default HelloModuleService ``` -To retrieve a workflow's execution details from the database, resolve the Workflow Engine Module from the container and use its `listWorkflowExecutions` method. +Then, add the following method that uses it: -For example, you can create a `GET` API Route at `src/workflows/[id]/route.ts` that retrieves a workflow execution for the specified transaction ID: +```ts highlights={repoHighlights} +// ... +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" -```ts title="src/workflows/[id]/route.ts" highlights={retrieveHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework" -import { Modules } from "@medusajs/framework/utils" +class HelloModuleService { + // ... + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + return await this.baseRepository_.transaction( + async (transactionManager) => { + await transactionManager.nativeUpdate( + "my_custom", + { + id: input.id, + }, + { + name: input.name, + } + ) -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { transaction_id } = req.params - - const workflowEngineService = req.scope.resolve( - Modules.WORKFLOW_ENGINE - ) + // retrieve again + const updatedRecord = await transactionManager.execute( + `SELECT * FROM my_custom WHERE id = '${input.id}'` + ) - const [workflowExecution] = await workflowEngineService.listWorkflowExecutions({ - transaction_id: transaction_id, - }) + return updatedRecord + }, + { + transaction: sharedContext.transactionManager, + } + ) + } - res.json({ - workflowExecution, - }) + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ) { + return await this.update_(input, sharedContext) + } } ``` -In the above example, you resolve the Workflow Engine Module from the container and use its `listWorkflowExecutions` method, passing the `transaction_id` as a filter to retrieve its workflow execution details. +The `update_` method uses the `baseRepository_.transaction` method to wrap a function in a transaction. -A workflow execution object will be similar to the following: +The function parameter receives a transactional entity manager as a parameter. Use it to perform the database operations. -```json -{ - "workflow_id": "hello-workflow", - "transaction_id": "01JJC2T6AVJCQ3N4BRD1EB88SP", - "id": "wf_exec_01JJC2T6B3P76JD35F12QTTA78", - "execution": { - "state": "done", - "steps": {}, - "modelId": "hello-workflow", - "options": {}, - "metadata": {}, - "startedAt": 1737719880027, - "definition": {}, - "timedOutAt": null, - "hasAsyncSteps": false, - "transactionId": "01JJC2T6AVJCQ3N4BRD1EB88SP", - "hasFailedSteps": false, - "hasSkippedSteps": false, - "hasWaitingSteps": false, - "hasRevertedSteps": false, - "hasSkippedOnFailureSteps": false - }, - "context": { - "data": {}, - "errors": [] - }, - "state": "done", - "created_at": "2025-01-24T09:58:00.036Z", - "updated_at": "2025-01-24T09:58:00.046Z", - "deleted_at": null +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. + +Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. + +### 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 + ): Promise { + return await this.baseRepository_.transaction( + async (transactionManager) => { + // ... + }, + { + transaction: sharedContext.transactionManager, + } + ) + } } ``` -### Example: Check if Stored Workflow Execution Failed +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` -To check if a stored workflow execution failed, you can check its `state` property: +```ts highlights={[["19"]]} +// other imports... +import { IsolationLevel } from "@mikro-orm/core" -```ts -if (workflowExecution.state === "failed") { - return res.status(500).json({ - error: "Workflow failed", - }) +class HelloModuleService { + // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + return await this.baseRepository_.transaction( + async (transactionManager) => { + // ... + }, + { + isolationLevel: IsolationLevel.READ_COMMITTED, + } + ) + } } ``` -Other state values include `done`, `invoking`, and `compensating`. +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 + ): Promise { + return await this.baseRepository_.transaction( + async (transactionManager) => { + // ... + }, + { + enableNestedTransactions: false, + } + ) + } +} +``` -# Workflow Hooks -In this chapter, you'll learn what a workflow hook is and how to consume them. +# Module Isolation -## What is a Workflow Hook? +In this chapter, you'll learn how modules are isolated, and what that means for your custom development. -A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler. +- 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](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), to extend a module's data models and retrieve data across modules. -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. +## How are Modules Isolated? -Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks. +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. -You want to perform a custom action during a workflow's execution, such as when a product is created. +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. *** -## How to Consume a Hook? - -A workflow has a special `hooks` property which is an object that holds its hooks. +## Why are Modules Isolated -So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory: +Some of the module isolation's benefits include: -- Import the workflow. -- Access its hook using the `hooks` property. -- Pass the hook a step function as a parameter to consume it. +- 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. -For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content: +*** -```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights} -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +## How to Extend Data Model of Another Module? -createProductsWorkflow.hooks.productsCreated( - async ({ products }, { container }) => { - // TODO perform an action - } -) -``` +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](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). -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. +## How to Use Services of Other Modules? -Now, when a product is created using the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), your hook handler is executed after the product is created. +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. -A hook can have only one handler. +Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. -Refer to the [createProductsWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) to see at which point the hook handler is executed. +### Example -### Hook Handler Parameter +For example, consider you have two modules: -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. +1. A module that stores and manages brands in your application. +2. A module that integrates a third-party Content Management System (CMS). -Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product. +To sync brands from your application to the third-party system, create the following steps: -### Hook Handler Compensation +```ts title="Example Steps" highlights={stepsHighlights} +const retrieveBrandsStep = createStep( + "retrieve-brands", + async (_, { container }) => { + const brandModuleService = container.resolve( + "brandModuleService" + ) -Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook. + const brands = await brandModuleService.listBrands() -For example: + return new StepResponse(brands) + } +) -```ts title="src/workflows/hooks/product-created.ts" -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +const createBrandsInCmsStep = createStep( + "create-brands-in-cms", + async ({ brands }, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" + ) -createProductsWorkflow.hooks.productsCreated( - async ({ products }, { container }) => { - // TODO perform an action + const cmsBrands = await cmsModuleService.createBrands(brands) - return new StepResponse(undefined, { ids }) + return new StepResponse(cmsBrands, cmsBrands) }, - async ({ ids }, { container }) => { - // undo the performed action + async (brands, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" + ) + + await cmsModuleService.deleteBrands( + brands.map((brand) => brand.id) + ) } ) ``` -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 +The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module. -Medusa's workflows pass in the hook's input an `additional_data` property: +Then, create the following workflow that uses these steps: -```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]} -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +```ts title="Example Workflow" +export const syncBrandsWorkflow = createWorkflow( + "sync-brands", + () => { + const brands = retrieveBrandsStep() -createProductsWorkflow.hooks.productsCreated( - async ({ products, additional_data }, { container }) => { - // TODO perform an action + createBrandsInCmsStep({ brands }) } ) ``` -This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow. +You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. -Learn how to pass `additional_data` in requests to API routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md). -### Pass Additional Data to Workflow +# Module Container -You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow: +In this chapter, you'll learn about the module's container and how to resolve resources in that container. -```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" +Since modules are isolated, each module has a local container only used by the resources of that module. -export async function POST(req: MedusaRequest, res: MedusaResponse) { - await createProductsWorkflow(req.scope).run({ - input: { - products: [ - // ... - ], - additional_data: { - custom_field: "test", - }, - }, - }) -} -``` +So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container. -Your hook handler then receives that passed data in the `additional_data` object. +### List of Registered Resources +Find a list of resources or dependencies registered in a module's container in [the Container Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). -# Variable Manipulation in Workflows with transform +*** -In this chapter, you'll learn how to use `transform` from the Workflows SDK to manipulate variables in a workflow. +## Resolve Resources -## Why Variable Manipulation isn't Allowed in Workflows +### Services -Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. +A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. -At that point, variables in the workflow don't have any values. They only do when you execute the workflow. +For example: -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 `transform` from the Workflows SDK. - -Restrictions for variable manipulation is only applicable in a workflow's definition. You can still manipulate variables in your step's code. - -*** - -## What is the transform Utility? - -`transform` 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 concatenate the strings, you create a new variable `str3` using the `transform` function: +```ts highlights={[["4"], ["10"]]} +import { Logger } from "@medusajs/framework/types" -```ts highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - transform, -} from "@medusajs/framework/workflows-sdk" -// step imports... +type InjectedDependencies = { + logger: Logger +} -const myWorkflow = createWorkflow( - "hello-world", - function (input) { - const str1 = step1(input) - const str2 = step2(input) +export default class HelloModuleService { + protected logger_: Logger - const str3 = transform( - { str1, str2 }, - (data) => `${data.str1}${data.str2}` - ) + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger - return new WorkflowResponse(str3) + this.logger_.info("[HelloModuleService]: Hello World!") } -) -``` - -`transform` 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 +### Loader -Use `transform` to loop over arrays to create another variable from the array's items. +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 collapsibleLines="1-7" expandButtonLabel="Show Imports" +```ts highlights={[["9"]]} +import { + LoaderOptions, +} from "@medusajs/framework/types" import { - createWorkflow, - WorkflowResponse, - transform, -} from "@medusajs/framework/workflows-sdk" -// step imports... - -type WorkflowInput = { - items: { - id: string - name: string - }[] -} + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -const myWorkflow = createWorkflow( - "hello-world", - function ({ items }: WorkflowInput) { - const ids = transform( - { items }, - (data) => data.items.map((item) => item.id) - ) - - doSomethingStep(ids) +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) - // ... - } -) + logger.info("[helloWorldLoader]: Hello, World!") +} ``` -This workflow receives an `items` array in its input. -You use `transform` to create an `ids` variable, which is an array of strings holding the `id` of each item in the `items` array. +# Loaders -You then pass the `ids` variable as a parameter to the `doSomethingStep`. +In this chapter, you’ll learn about loaders and how to use them. -*** +## What is a Loader? -## Example: Creating a Date +When building a commerce application, you'll often need to execute an action the first time the application starts. For example, if your application needs to connect to databases other than Medusa's PostgreSQL database, you might need to establish a connection on application startup. -If you create a date with `new Date()` in a workflow's constructor function, Medusa evaluates the date's value when it creates the internal representation of the workflow, not when the workflow is executed. +In Medusa, you can execute an action when the application starts using a loader. A loader is a function exported by a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of business logic for a single domain. When the Medusa application starts, it executes all loaders exported by configured modules. -So, use `transform` instead to create a date variable with `new Date()`. +Loaders are useful to register custom resources, such as database connections, in the [module's container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md), which is similar to the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) but includes only [resources available to the module](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). Modules are isolated, so they can't access resources outside of them, such as a service in another module. -For example: +Medusa isolates modules to ensure that they're re-usable across applications, aren't tightly coupled to other resources, and don't have implications when integrated into the Medusa application. Learn more about why modules are isolated in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), and check out [this reference for the list of resources in the module's container](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). -```ts -const myWorkflow = createWorkflow( - "hello-world", - () => { - const today = transform({}, () => new Date()) +*** - doSomethingStep(today) - } -) -``` +## How to Create a Loader? -In this workflow, `today` is only evaluated when the workflow is executed. +### 1. Implement Loader Function -*** +You create a loader function in a TypeScript or JavaScript file under a module's `loaders` directory. -## Caveats +For example, consider you have a `hello` module, you can create a loader at `src/modules/hello/loaders/hello-world.ts` with the following content: -### Transform Evaluation +![Example of loader file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732865671/Medusa%20Book/loader-dir-overview_eg6vtu.jpg) -`transform`'s value is only evaluated if you pass its output to a step or in the workflow response. +Learn how to create a module in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -For example, if you have the following workflow: +```ts title="src/modules/hello/loaders/hello-world.ts" +import { + LoaderOptions, +} from "@medusajs/framework/types" -```ts -const myWorkflow = createWorkflow( - "hello-world", - function (input) { - const str = transform( - { input }, - (data) => `${data.input.str1}${data.input.str2}` - ) +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const logger = container.resolve("logger") - return new WorkflowResponse("done") - } -) + logger.info("[helloWorldLoader]: Hello, World!") +} ``` -Since `str`'s value isn't used as a step's input or passed to `WorkflowResponse`, its value is never evaluated. - -### Data Validation +The loader file exports an async function, which is the function executed when the application loads. -`transform` should only be used to perform variable or data manipulation. +The function receives an object parameter that has a `container` property, which is the module's container that you can use to resolve resources from. In this example, you resolve the Logger utility to log a message in the terminal. -If you want to perform some validation on the data, use a step or [when-then](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md) instead. +Find the list of resources in the module's container in [this reference](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). -For example: +### 2. Export Loader in Module Definition -```ts -// DON'T -const myWorkflow = createWorkflow( - "hello-world", - function (input) { - const str = transform( - { input }, - (data) => { - if (!input.str1) { - throw new Error("Not allowed!") - } - } - ) - } -) +After implementing the loader, you must export it in the module's definition in the `index.ts` file at the root of the module's directory. Otherwise, the Medusa application will not run it. -// DO -const validateHasStr1Step = createStep( - "validate-has-str1", - ({ input }) => { - if (!input.str1) { - throw new Error("Not allowed!") - } - } -) +So, to export the loader you implemented above in the `hello` module, add the following to `src/modules/hello/index.ts`: -const myWorkflow = createWorkflow( - "hello-world", - function (input) { - validateHasStr1({ - input, - }) +```ts title="src/modules/hello/index.ts" +// other imports... +import helloWorldLoader from "./loaders/hello-world" - // workflow continues its execution only if - // the step doesn't throw the error. - } -) +export default Module("hello", { + // ... + loaders: [helloWorldLoader], +}) ``` +The second parameter of the `Module` function accepts a `loaders` property whose value is an array of loader functions. The Medusa application will execute these functions when it starts. -# Workflow Timeout - -In this chapter, you’ll learn how to set a timeout for workflows and steps. +### Test the Loader -## What is a Workflow Timeout? +Assuming your module is [added to Medusa's configuration](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you can test the loader by starting the Medusa application: -By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. +```bash npm2yarn +npm run dev +``` -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. +Then, you'll find the following message logged in the terminal: -### Timeout Doesn't Stop Step Execution +```plain +info: [HELLO MODULE] Just started the Medusa application! +``` -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. +This indicates that the loader in the `hello` module ran and logged this message. *** -## Configure Workflow Timeout +## Example: Register Custom MongoDB Connection -The `createWorkflow` function can accept a configuration object instead of the workflow’s name. +As mentioned in this chapter's introduction, loaders are most useful when you need to register a custom resource in the module's container to re-use it in other customizations in the module. -In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. +Consider your have a MongoDB module that allows you to perform operations on a MongoDB database. -For example: +### Prerequisites -```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" -import { - createStep, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +- [MongoDB database that you can connect to from a local machine.](https://www.mongodb.com) +- [Install the MongoDB SDK in your Medusa application.](https://www.mongodb.com/docs/drivers/node/current/quick-start/download-and-install/#install-the-node.js-driver) -const step1 = createStep( - "step-1", - async () => { - // ... - } -) +To connect to the database, you create the following loader in your module: -const myWorkflow = createWorkflow({ - name: "hello-world", - timeout: 2, // 2 seconds -}, function () { - const str1 = step1() - - return new WorkflowResponse({ - message: str1, - }) -}) +```ts title="src/modules/mongo/loaders/connection.ts" highlights={loaderHighlights} +import { LoaderOptions } from "@medusajs/framework/types" +import { asValue } from "awilix" +import { MongoClient } from "mongodb" -export default myWorkflow +type ModuleOptions = { + connection_url?: string + db_name?: string +} -``` +export default async function mongoConnectionLoader({ + container, + options, +}: LoaderOptions) { + if (!options.connection_url) { + throw new Error(`[MONGO MDOULE]: connection_url option is required.`) + } + if (!options.db_name) { + throw new Error(`[MONGO MDOULE]: db_name option is required.`) + } + const logger = container.resolve("logger") + + try { + const clientDb = ( + await (new MongoClient(options.connection_url)).connect() + ).db(options.db_name) -This workflow's executions fail if they run longer than two seconds. + logger.info("Connected to MongoDB") -A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/access-workflow-errors/index.html.md). The error’s name is `TransactionTimeoutError`. + container.register( + "mongoClient", + asValue(clientDb) + ) + } catch (e) { + logger.error( + `[MONGO MDOULE]: An error occurred while connecting to MongoDB: ${e}` + ) + } +} +``` -*** +The loader function accepts in its object parameter an `options` property, which is the options passed to the module in Medusa's configurations. For example: -## Configure Step Timeout +```ts title="medusa-config.ts" highlights={optionHighlights} +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/mongo", + options: { + connection_url: process.env.MONGO_CONNECTION_URL, + db_name: process.env.MONGO_DB_NAME, + }, + }, + ], +}) +``` -Alternatively, you can configure the timeout for a step rather than the entire workflow. +Passing options is useful when your module needs informations like connection URLs or API keys, as it ensures your module can be re-usable across applications. For the MongoDB Module, you expect two options: -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. +- `connection_url`: the URL to connect to the MongoDB database. +- `db_name`: The name of the database to connect to. -The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. +In the loader, you check first that these options are set before proceeding. Then, you create an instance of the MongoDB client and connect to the database specified in the options. -For example: +After creating the client, you register it in the module's container using the container's `register` method. The method accepts two parameters: -```tsx -const step1 = createStep( - { - name: "step-1", - timeout: 2, // 2 seconds - }, - async () => { - // ... - } -) -``` +1. The key to register the resource under, which in this case is `mongoClient`. You'll use this name later to resolve the client. +2. The resource to register in the container, which is the MongoDB client you created. However, you don't pass the resource as-is. Instead, you need to use an `asValue` function imported from the [awilix package](https://github.com/jeffijoe/awilix), which is the package used to implement the container functionality in Medusa. -This step's executions fail if they run longer than two seconds. +### Use Custom Registered Resource in Module's Service -A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/access-workflow-errors/index.html.md). The error’s name is `TransactionStepTimeoutError`. +After registering the custom MongoDB client in the module's container, you can now resolve and use it in the module's service. +For example: -# Write Tests for Modules +```ts title="src/modules/mongo/service.ts" +import type { Db } from "mongodb" -In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests for a module's main service. +type InjectedDependencies = { + mongoClient: Db +} -### Prerequisites +export default class MongoModuleService { + private mongoClient_: Db -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) + constructor({ mongoClient }: InjectedDependencies) { + this.mongoClient_ = mongoClient + } -## moduleIntegrationTestRunner Utility + async createMovie({ title }: { + title: string + }) { + const moviesCol = this.mongoClient_.collection("movie") -`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. + const insertedMovie = await moviesCol.insertOne({ + title, + }) -For example, assuming you have a `hello` module, create a test file at `src/modules/hello/__tests__/service.spec.ts`: + const movie = await moviesCol.findOne({ + _id: insertedMovie.insertedId, + }) -```ts title="src/modules/hello/__tests__/service.spec.ts" -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import { HELLO_MODULE } from ".." -import HelloModuleService from "../service" -import MyCustom from "../models/my-custom" + return movie + } -moduleIntegrationTestRunner({ - moduleName: HELLO_MODULE, - moduleModels: [MyCustom], - resolve: "./src/modules/hello", - testSuite: ({ service }) => { - // TODO write tests - }, -}) + async deleteMovie(id: string) { + const moviesCol = this.mongoClient_.collection("movie") -jest.setTimeout(60 * 1000) + await moviesCol.deleteOne({ + _id: { + equals: id, + }, + }) + } +} ``` -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 service `MongoModuleService` resolves the `mongoClient` resource you registered in the loader and sets it as a class property. You then use it in the `createMovie` and `deleteMovie` methods, which create and delete a document in a `movie` collection in the MongoDB database, respectively. -The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service. +Make sure to export the loader in the module's definition in the `index.ts` file at the root directory of the module: -The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property. +```ts title="src/modules/mongo/index.ts" highlights={[["9"]]} +import { Module } from "@medusajs/framework/utils" +import MongoModuleService from "./service" +import mongoConnectionLoader from "./loaders/connection" -The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). +export const MONGO_MODULE = "mongo" -*** +export default Module(MONGO_MODULE, { + service: MongoModuleService, + loaders: [mongoConnectionLoader], +}) +``` -## Run Tests +### Test it Out -Run the following command to run your module integration tests: +You can test the connection out by starting the Medusa application. If it's successful, you'll see the following message logged in the terminal: -```bash npm2yarn -npm run test:integration:modules +```bash +info: Connected to MongoDB ``` -If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). - -This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. +You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database. -*** -## Pass Module Options +# Modules Directory Structure -If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter. +In this document, you'll learn about the expected files and directories in your module. -For example: +![Module Directory Structure Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1714379976/Medusa%20Book/modules-dir-overview_nqq7ne.jpg) -```ts -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import HelloModuleService from "../service" +## index.ts -moduleIntegrationTestRunner({ - moduleOptions: { - apiKey: "123", - }, - // ... -}) -``` +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](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). *** -## Write Tests for Modules without Data Models +## service.ts -If your module doesn't have a data model, pass a dummy model in the `moduleModels` property. +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](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -For example: +*** -```ts -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import HelloModuleService from "../service" -import { model } from "@medusajs/framework/utils" +## Other Directories -const DummyModel = model.define("dummy_model", { - id: model.id().primaryKey(), -}) +The following directories are optional and their content are explained more in the following chapters: -moduleIntegrationTestRunner({ - moduleModels: [DummyModel], - // ... -}) +- `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. -jest.setTimeout(60 * 1000) -``` -*** +# Module Options -### Other Options and Inputs +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. -Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. +## 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. *** -## Database Used in Tests +## How to Pass Options to a Module? -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 pass options to a module, add an `options` property to the module’s configuration in `medusa-config.ts`. -To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). +For example: +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/hello", + options: { + capitalize: true, + }, + }, + ], +}) +``` -# Write Integration Tests +The `options` property’s value is an object. You can pass any properties you want. -In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests. +### Pass Options to a Module in a Plugin -### Prerequisites - -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) - -## medusaIntegrationTestRunner Utility - -The `medusaIntegrationTestRunner` is from Medusa's Testing Framework and it's used to create integration tests in your Medusa project. It runs a full Medusa application, allowing you test API routes, workflows, or other customizations. +If your module is part of a plugin, you can pass options to the module in the plugin’s configuration. For example: -```ts title="integration-tests/http/test.spec.ts" highlights={highlights} -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" - -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - // TODO write tests... - }, +```ts title="medusa-config.ts" +import { defineConfig } from "@medusajs/framework/utils" +module.exports = defineConfig({ + plugins: [ + { + resolve: "@myorg/plugin-name", + options: { + capitalize: true, + }, + }, + ], }) - -jest.setTimeout(60 * 1000) ``` -The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`. +The `options` property in the plugin configuration is passed to all modules in a plugin. -`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. +## Access Module Options in Main Service -The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). +The module’s main service receives the module options as a second parameter. -### Jest Timeout +For example: -Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test: +```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" -```ts title="integration-tests/http/test.spec.ts" -// in your test's file -jest.setTimeout(60 * 1000) -``` +// recommended to define type in another file +type ModuleOptions = { + capitalize?: boolean +} -*** +export default class HelloModuleService extends MedusaService({ + MyCustom, +}){ + protected options_: ModuleOptions -### Run Tests + constructor({}, options?: ModuleOptions) { + super(...arguments) -Run the following command to run your tests: + this.options_ = options || { + capitalize: false, + } + } -```bash npm2yarn -npm run test:integration + // ... +} ``` -If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). - -This runs your Medusa application and runs the tests available under the `src/integrations/http` directory. - *** -## Other Options and Inputs +## Access Module Options in Loader -Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. +The object that a module’s loaders receive as a parameter has an `options` property holding the module's options. -*** +For example: -## Database Used in Tests +```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" -The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. +// recommended to define type in another file +type ModuleOptions = { + capitalize?: boolean +} -To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md). +export default async function helloWorldLoader({ + options, +}: LoaderOptions) { + + console.log( + "[HELLO MODULE] Just started the Medusa application!", + options + ) +} +``` *** -## Example Integration Tests - -The next chapters provide examples of writing integration tests for API routes and workflows. - - -# Guide: Add Product's Brand Widget in Admin +## Validate Module Options -In this chapter, you'll customize the product details page of the Medusa Admin dashboard to show the product's [brand](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md). You'll create a widget that is injected into a pre-defined zone in the page, and in the widget you'll retrieve the product's brand from the server and display it. +If you expect a certain option and want to throw an error if it's not provided or isn't valid, it's recommended to perform the validation in a loader. The module's service is only instantiated when it's used, whereas the loader runs the when the Medusa application starts. -### Prerequisites +So, by performing the validation in the loader, you ensure you can throw an error at an early point, rather than when the module is used. -- [Brands linked to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) +For example, to validate that the Hello Module received an `apiKey` option, create the loader `src/modules/loaders/validate.ts`: -## 1. Initialize JS SDK +```ts title="src/modules/hello/loaders/validate.ts" +import { LoaderOptions } from "@medusajs/framework/types" +import { MedusaError } from "@medusajs/framework/utils" -In your custom widget, you'll retrieve the product's brand by sending a request to the Medusa server. Medusa has a [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) that simplifies sending requests to the server's API routes. +// recommended to define type in another file +type ModuleOptions = { + apiKey?: string +} -So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content: +export default async function validationLoader({ + options, +}: LoaderOptions) { + if (!options.apiKey) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Hello Module requires an apiKey option." + ) + } +} +``` -![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg) +Then, export the loader in the module's definition file, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md): -```ts title="src/admin/lib/sdk.ts" -import Medusa from "@medusajs/js-sdk" +```ts title="src/modules/hello/index.ts" +// other imports... +import validationLoader from "./loaders/validate" -export const sdk = new Medusa({ - baseUrl: import.meta.env.VITE_BACKEND_URL || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", - }, +export default Module("hello", { + // ... + loaders: [validationLoader], }) ``` -You initialize the SDK passing it the following options: +Now, when the Medusa application starts, the loader will run, validating the module's options and throwing an error if the `apiKey` option is missing. -- `baseUrl`: The URL to the Medusa server. -- `debug`: Whether to enable logging debug messages. This should only be enabled in development. -- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard. -Notice that you use `import.meta.env` to access environment variables in your customizations because the Medusa Admin is built on top of Vite. Learn more in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). +# Multiple Services in a Module -You can now use the SDK to send requests to the Medusa server. +In this chapter, you'll learn how to use multiple services in a module. -Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). +## Module's Main and Internal Services -*** +A module has one main service only, which is the service exported in the module's definition. -## 2. Add Widget to Product Details Page +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. -You'll now add a widget to the product-details page. A widget is a React component that's injected into pre-defined zones in the Medusa Admin dashboard. It's created in a `.tsx` file under the `src/admin/widgets` directory. +*** -Learn more about widgets in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md). +## How to Add an Internal Service -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: +### 1. Create Service -![Directory structure of the Medusa application after adding the widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414684/Medusa%20Book/brands-admin-dir-overview-2_eq5xhi.jpg) +To add an internal service, create it in the `services` directory of your module. -```tsx title="src/admin/widgets/product-brand.tsx" highlights={highlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { DetailWidgetProps, AdminProduct } from "@medusajs/framework/types" -import { clx, Container, Heading, Text } from "@medusajs/ui" -import { useQuery } from "@tanstack/react-query" -import { sdk } from "../lib/sdk" +For example, create the file `src/modules/hello/services/client.ts` with the following content: -type AdminProductBrand = AdminProduct & { - brand?: { - id: string - name: string +```ts title="src/modules/hello/services/client.ts" +export class ClientService { + async getMessage(): Promise { + return "Hello, World!" } } +``` -const ProductBrandWidget = ({ - data: product, -}: DetailWidgetProps) => { - const { data: queryResult } = useQuery({ - queryFn: () => sdk.admin.product.retrieve(product.id, { - fields: "+brand.*", - }), - queryKey: [["product", product.id]], - }) - const brandName = (queryResult?.product as AdminProductBrand)?.brand?.name - - return ( - -
-
- Brand -
-
-
- - Name - +### 2. Export Service in Index - - {brandName || "-"} - -
-
- ) -} +Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) +For example, create the file `src/modules/hello/services/index.ts` with the following content: -export default ProductBrandWidget +```ts title="src/modules/hello/services/index.ts" +export * from "./client" ``` -A widget's file must export: - -- A React component to be rendered in the specified injection zone. The component must be the file's default export. -- A configuration object created with `defineWidgetConfig` from the Admin Extension SDK. The function receives an object as a parameter that has a `zone` property, whose value is the zone to inject the widget to. - -Since the widget is injected at the top of the product details page, the widget receives the product's details as a parameter. +This exports the `ClientService`. -In the widget, you use [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. In the `queryFn` function that executes the query, you use the JS SDK to send a request to the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid), passing `+brand.*` in the `fields` query parameter to retrieve the product's brand. +### 3. Resolve Internal Service -Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. +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. -You then render a section that shows the brand's name. In admin customizations, use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md) to maintain a consistent user interface and design in the dashboard. +For example, in your main service: -*** +```ts title="src/modules/hello/service.ts" highlights={[["5"], ["13"]]} +// other imports... +import { ClientService } from "./services" -## Test it Out +type InjectedDependencies = { + clientService: ClientService +} -To test out your widget, start the Medusa application: +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + protected clientService_: ClientService -```bash npm2yarn -npm run dev + constructor({ clientService }: InjectedDependencies) { + super(...arguments) + this.clientService_ = clientService + } +} ``` -Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, open the page of a product that has a brand. You'll see a new section at the top showing the brand's name. - -![The widget is added as the first section of the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414415/Medusa%20Book/Screenshot_2024-12-05_at_5.59.25_PM_y85m14.png) +You can now use your internal service in your main service. *** -## Admin Components Guides +## Resolve Resources in Internal Service -When building your widget, you may need more complicated components. For example, you may add a form to the above widget to set the product's brand. +Resolve dependencies from your module's container in the constructor of your internal service. -The [Admin Components guides](https://docs.medusajs.com/resources/admin-components/index.html.md) show you how to build and use common components in the Medusa Admin, such as forms, tables, JSON data viewer, and more. The components in the guides also follow the Medusa Admin's design convention. +For example: -*** +```ts +import { Logger } from "@medusajs/framework/types" -## Next Chapter: Add UI Route for Brands +type InjectedDependencies = { + logger: Logger +} -In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users. +export class ClientService { + protected logger_: Logger + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger + } +} +``` -# Create Brands UI Route in Admin +*** -In this chapter, you'll add a UI route to the admin dashboard that shows all [brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) in a new page. You'll retrieve the brands from the server and display them in a table with pagination. +## Access Module Options -### Prerequisites +Your internal service can't access the module's options. -- [Brands Module](https://docs.medusajs.com/learn/customization/custom-features/modules/index.html.md) +To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`. -## 1. Get Brands API Route +For example: -In a [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/query-linked-records/index.html.md), you learned how to add an API route that retrieves brands and their products using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll expand that API route to support pagination, so that on the admin dashboard you can show the brands in a paginated table. +```ts +import { ConfigModule } from "@medusajs/framework/types" +import { HELLO_MODULE } from ".." -Replace or create the `GET` API route at `src/api/admin/brands/route.ts` with the following: +export type InjectedDependencies = { + configModule: ConfigModule +} -```ts title="src/api/admin/brands/route.ts" highlights={apiRouteHighlights} -// other imports... -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +export class ClientService { + protected options: Record -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve("query") - - const { - data: brands, - metadata: { count, take, skip } = {}, - } = await query.graph({ - entity: "brand", - ...req.queryConfig, - }) + constructor({ configModule }: InjectedDependencies) { + const moduleDef = configModule.modules[HELLO_MODULE] - res.json({ - brands, - count, - limit: take, - offset: skip, - }) + if (typeof moduleDef !== "boolean") { + this.options = moduleDef.options + } + } } ``` -In the API route, you use Query's `graph` method to retrieve the brands. In the method's object parameter, you spread the `queryConfig` property of the request object. This property holds configurations for pagination and retrieved fields. - -The query configurations are combined from default configurations, which you'll add next, and the request's query parameters: +The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. -- `fields`: The fields to retrieve in the brands. -- `limit`: The maximum number of items to retrieve. -- `offset`: The number of items to skip before retrieving the returned items. +If its value is not a `boolean`, set the service's options to the module configuration's `options` property. -When you pass pagination configurations to the `graph` method, the returned object has the pagination's details in a `metadata` property, whose value is an object having the following properties: -- `count`: The total count of items. -- `take`: The maximum number of items returned in the `data` array. -- `skip`: The number of items skipped before retrieving the returned items. +# Service Constraints -You return in the response the retrieved brands and the pagination configurations. +This chapter lists constraints to keep in mind when creating a service. -Learn more about pagination with Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-pagination/index.html.md). +## Use Async Methods -*** +Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronous, it always executes methods in the wrapper with the `await` keyword. -## 2. Add Default Query Configurations +For example, if you have a synchronous `getMessage` method, and you use it in other resources like workflows, Medusa executes it as an async method: -Next, you'll set the default query configurations of the above API route and allow passing query parameters to change the configurations. +```ts +await helloModuleService.getMessage() +``` -Medusa provides a `validateAndTransformQuery` middleware that validates the accepted query parameters for a request and sets the default Query configuration. So, in `src/api/middlewares.ts`, add a new middleware configuration object: +So, make sure your service's methods are always async to avoid unexpected errors or behavior. -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - validateAndTransformQuery, -} from "@medusajs/framework/http" -import { createFindParams } from "@medusajs/medusa/api/utils/validators" -// other imports... +```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" -export const GetBrandsSchema = createFindParams() +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + // Don't + getMessage(): string { + return "Hello, World!" + } -export default defineMiddlewares({ - routes: [ - // ... - { - matcher: "/admin/brands", - method: "GET", - middlewares: [ - validateAndTransformQuery( - GetBrandsSchema, - { - defaults: [ - "id", - "name", - "products.*", - ], - isList: true, - } - ), - ], - }, + // Do + async getMessage(): Promise { + return "Hello, World!" + } +} - ], -}) +export default HelloModuleService ``` -You apply the `validateAndTransformQuery` middleware on the `GET /admin/brands` API route. The middleware accepts two parameters: -- A [Zod](https://zod.dev/) schema that a request's query parameters must satisfy. Medusa provides `createFindParams` that generates a Zod schema with the following properties: - - `fields`: A comma-separated string indicating the fields to retrieve. - - `limit`: The maximum number of items to retrieve. - - `offset`: The number of items to skip before retrieving the returned items. - - `order`: The name of the field to sort the items by. Learn more about sorting in [the API reference](https://docs.medusajs.com/api/admin#sort-order) -- An object of Query configurations having the following properties: - - `defaults`: An array of default fields and relations to retrieve. - - `isList`: Whether the API route returns a list of items. +# Access Workflow Errors -By applying the above middleware, you can pass pagination configurations to `GET /admin/brands`, which will return a paginated list of brands. You'll see how it works when you create the UI route. +In this chapter, you’ll learn how to access errors that occur during a workflow’s execution. -Learn more about using the `validateAndTransformQuery` middleware to configure Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query#request-query-configurations/index.html.md). +## How to Access Workflow Errors? -*** +By default, when an error occurs in a workflow, it throws that error, and the execution stops. -## 3. Initialize JS SDK +You can configure the workflow to return the errors instead so that you can access and handle them differently. -In your custom UI route, you'll retrieve the brands by sending a request to the Medusa server. Medusa has a [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) that simplifies sending requests to the core API route. +For example: -If you didn't follow the [previous chapter](https://docs.medusajs.com/learn/customization/customize-admin/widget/index.html.md), create the file `src/admin/lib/sdk.ts` with the following content: +```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" -![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg) +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result, errors } = await myWorkflow(req.scope) + .run({ + // ... + throwOnError: false, + }) -```ts title="src/admin/lib/sdk.ts" -import Medusa from "@medusajs/js-sdk" + if (errors.length) { + return res.send({ + errors: errors.map((error) => error.error), + }) + } + + res.send(result) +} -export const sdk = new Medusa({ - baseUrl: import.meta.env.VITE_BACKEND_URL || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", - }, -}) ``` -You initialize the SDK passing it the following options: +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. -- `baseUrl`: The URL to the Medusa server. -- `debug`: Whether to enable logging debug messages. This should only be enabled in development. -- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard. +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. -Notice that you use `import.meta.env` to access environment variables in your customizations because the Medusa Admin is built on top of Vite. Learn more in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). -You can now use the SDK to send requests to the Medusa server. +# Service Factory -Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). +In this chapter, you’ll learn about what the service factory is and how to use it. -*** +## What is the Service Factory? -## 4. Add a UI Route to Show Brands +Medusa provides a service factory that your module’s main service can extend. -You'll now add the UI route that shows the paginated list of brands. A UI route is a React component created in a `page.tsx` file under a sub-directory of `src/admin/routes`. The file's path relative to src/admin/routes determines its path in the dashboard. +The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually. -Learn more about UI routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). +Your service provides data-management functionalities of your data models. -So, to add the UI route at the `localhost:9000/app/brands` path, create the file `src/admin/routes/brands/page.tsx` with the following content: +*** -![Directory structure of the Medusa application after adding the UI route.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733472011/Medusa%20Book/brands-admin-dir-overview-3_syytld.jpg) +## How to Extend the Service Factory? -```tsx title="src/admin/routes/brands/page.tsx" highlights={uiRouteHighlights} -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { TagSolid } from "@medusajs/icons" -import { - Container, -} from "@medusajs/ui" -import { useQuery } from "@tanstack/react-query" -import { sdk } from "../../lib/sdk" -import { useMemo, useState } from "react" +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. -const BrandsPage = () => { - // TODO retrieve brands +For example, create the file `src/modules/hello/service.ts` with the following content: - return ( - - {/* TODO show brands */} - - ) -} +```ts title="src/modules/hello/service.ts" highlights={highlights} +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" -export const config = defineRouteConfig({ - label: "Brands", - icon: TagSolid, -}) +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + // TODO implement custom methods +} -export default BrandsPage +export default HelloModuleService ``` -A route's file must export the React component that will be rendered in the new page. It must be the default export of the file. You can also export configurations that add a link in the sidebar for the UI route. You create these configurations using `defineRouteConfig` from the Admin Extension SDK. - -So far, you only show a container. In admin customizations, use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md) to maintain a consistent user interface and design in the dashboard. +### MedusaService Parameters -### Retrieve Brands From API Route +The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for. -You'll now update the UI route to retrieve the brands from the API route you added earlier. +In the example above, since the `HelloModuleService` extends `MedusaService`, it has methods to manage the `MyCustom` data model, such as `createMyCustoms`. -First, add the following type in `src/admin/routes/brands/page.tsx`: +### Generated Methods -```tsx title="src/admin/routes/brands/page.tsx" -type Brand = { - id: string - name: string -} -type BrandsResponse = { - brands: Brand[] - count: number - limit: number - offset: number -} -``` +The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database. -You define the type for a brand, and the type of expected response from the `GET /admin/brands` API route. +The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`. -To display the brands, you'll use Medusa UI's [DataTable](https://docs.medusajs.com/ui/components/data-table/index.html.md) component. So, add the following imports in `src/admin/routes/brands/page.tsx`: +For example, the following methods are generated for the service above: -```tsx title="src/admin/routes/brands/page.tsx" -import { - // ... - Heading, - createDataTableColumnHelper, - DataTable, - DataTablePaginationState, - useDataTable, -} from "@medusajs/ui" -``` +Find a complete reference of each of the methods in [this documentation](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) -You import the `DataTable` component and the following utilities: +### listMyCustoms -- `createDataTableColumnHelper`: A utility to create columns for the data table. -- `DataTablePaginationState`: A type that holds the pagination state of the data table. -- `useDataTable`: A hook to initialize and configure the data table. +### listMyCustoms -You also import the `Heading` component to show a heading above the data table. +This method retrieves an array of records based on filters and pagination configurations. -Next, you'll define the table's columns. Add the following before the `BrandsPage` component: +For example: -```tsx title="src/admin/routes/brands/page.tsx" -const columnHelper = createDataTableColumnHelper() +```ts +const myCustoms = await helloModuleService + .listMyCustoms() -const columns = [ - columnHelper.accessor("id", { - header: "ID", - }), - columnHelper.accessor("name", { - header: "Name", - }), -] +// with filters +const myCustoms = await helloModuleService + .listMyCustoms({ + id: ["123"] + }) ``` -You use the `createDataTableColumnHelper` utility to create columns for the data table. You define two columns for the ID and name of the brands. +### listAndCount -Then, replace the `// TODO retrieve brands` in the component with the following: +### retrieveMyCustom -```tsx title="src/admin/routes/brands/page.tsx" highlights={queryHighlights} -const limit = 15 -const [pagination, setPagination] = useState({ - pageSize: limit, - pageIndex: 0, -}) -const offset = useMemo(() => { - return pagination.pageIndex * limit -}, [pagination]) +This method retrieves a record by its ID. -const { data, isLoading } = useQuery({ - queryFn: () => sdk.client.fetch(`/admin/brands`, { - query: { - limit, - offset, - }, - }), - queryKey: [["brands", limit, offset]], -}) +For example: -// TODO configure data table +```ts +const myCustom = await helloModuleService + .retrieveMyCustom("123") ``` -To enable pagination in the `DataTable` component, you need to define a state variable of type `DataTablePaginationState`. It's an object having the following properties: +### retrieveMyCustom -- `pageSize`: The maximum number of items per page. You set it to `15`. -- `pageIndex`: A zero-based index of the current page of items. +### updateMyCustoms -You also define a memoized `offset` value that indicates the number of items to skip before retrieving the current page's items. +This method updates and retrieves records of the data model. -Then, you use `useQuery` from [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. +For example: -Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. +```ts +const myCustom = await helloModuleService + .updateMyCustoms({ + id: "123", + name: "test" + }) -In the `queryFn` function that executes the query, you use the JS SDK's `client.fetch` method to send a request to your custom API route. The first parameter is the route's path, and the second is an object of request configuration and data. You pass the query parameters in the `query` property. +// update multiple +const myCustoms = await helloModuleService + .updateMyCustoms([ + { + id: "123", + name: "test" + }, + { + id: "321", + name: "test 2" + }, + ]) -This sends a request to the [Get Brands API route](#1-get-brands-api-route), passing the pagination query parameters. Whenever `currentPage` is updated, the `offset` is also updated, which will send a new request to retrieve the brands for the current page. +// use filters +const myCustoms = await helloModuleService + .updateMyCustoms([ + { + selector: { + id: ["123", "321"] + }, + data: { + name: "test" + } + }, + ]) +``` -### Display Brands Table +### createMyCustoms -Finally, you'll display the brands in a data table. Replace the `// TODO configure data table` in the component with the following: +### softDeleteMyCustoms -```tsx title="src/admin/routes/brands/page.tsx" -const table = useDataTable({ - columns, - data: data?.brands || [], - getRowId: (row) => row.id, - rowCount: data?.count || 0, - isLoading, - pagination: { - state: pagination, - onPaginationChange: setPagination, - }, -}) -``` +This method soft-deletes records using an array of IDs or an object of filters. -You use the `useDataTable` hook to initialize and configure the data table. It accepts an object with the following properties: +For example: -- `columns`: The columns of the data table. You created them using the `createDataTableColumnHelper` utility. -- `data`: The brands to display in the table. -- `getRowId`: A function that returns a unique identifier for a row. -- `rowCount`: The total count of items. This is used to determine the number of pages. -- `isLoading`: A boolean indicating whether the data is loading. -- `pagination`: An object to configure pagination. It accepts the following properties: - - `state`: The pagination state of the data table. - - `onPaginationChange`: A function to update the pagination state. +```ts +await helloModuleService.softDeleteMyCustoms("123") -Then, replace the `{/* TODO show brands */}` in the return statement with the following: +// soft-delete multiple +await helloModuleService.softDeleteMyCustoms([ + "123", "321" +]) -```tsx title="src/admin/routes/brands/page.tsx" - - - Brands - - - - +// use filters +await helloModuleService.softDeleteMyCustoms({ + id: ["123", "321"] +}) ``` -This renders the data table that shows the brands with pagination. The `DataTable` component accepts the `instance` prop, which is the object returned by the `useDataTable` hook. +### updateMyCustoms -*** +### deleteMyCustoms -## Test it Out +### softDeleteMyCustoms -To test out the UI route, start the Medusa application: +### restoreMyCustoms -```bash npm2yarn -npm run dev -``` +### Using a Constructor -Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, you'll find a new "Brands" sidebar item. Click on it to see the brands in your store. You can also go to `http://localhost:9000/app/brands` to see the page. +If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`. -![A new sidebar item is added for the new brands UI route. The UI route shows the table of brands with pagination.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733421074/Medusa%20Book/Screenshot_2024-12-05_at_7.46.52_PM_slcdqd.png) +For example: -*** +```ts highlights={[["8"]]} +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" -## Summary +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + constructor() { + super(...arguments) + } +} -By following the previous chapters, you: +export default HelloModuleService +``` -- Injected a widget into the product details page to show the product's brand. -- Created a UI route in the Medusa Admin that shows the list of brands. -*** +# Scheduled Jobs Number of Executions -## Next Steps: Integrate Third-Party Systems +In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. -Your customizations often span across systems, where you need to retrieve data or perform operations in a third-party system. +## numberOfExecutions Option -In the next chapters, you'll learn about the concepts that facilitate integrating third-party systems in your application. You'll integrate a dummy third-party system and sync the brands between it and the Medusa application. +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: -# Guide: Create Brand API Route +```ts highlights={highlights} +export default async function myCustomJob() { + console.log("I'll be executed three times only.") +} -In the previous two chapters, you created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that added the concepts of brands to your application, then created a [workflow to create a brand](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). In this chapter, you'll expose an API route that allows admin users to create a brand using the workflow from the previous chapter. +export const config = { + name: "hello-world", + // execute every minute + schedule: "* * * * *", + numberOfExecutions: 3, +} +``` -An API Route is an endpoint that acts as an entry point for other clients to interact with your Medusa customizations, such as the admin dashboard, storefronts, or third-party systems. +The above scheduled job has the `numberOfExecutions` configuration set to `3`. -The Medusa core application provides a set of [admin](https://docs.medusajs.com/api/admin) and [store](https://docs.medusajs.com/api/store) API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities. +So, it'll only execute 3 times, each every minute, then it won't be executed anymore. -### Prerequisites +If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. -- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) -## 1. Create the API Route +# Expose a Workflow Hook -You create an API route in a `route.{ts,js}` file under a sub-directory of the `src/api` directory. The file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). +In this chapter, you'll learn how to expose a hook in your workflow. -Learn more about API routes [in this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md). +## When to Expose a Hook -The route's path is the path of `route.{ts,js}` relative to `src/api`. So, to create the API route at `/admin/brands`, create the file `src/api/admin/brands/route.ts` with the following content: +Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow. -![Directory structure of the Medusa application after adding the route](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869882/Medusa%20Book/brand-route-dir-overview-2_hjqlnf.jpg) +Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead. -```ts title="src/api/admin/brands/route.ts" +*** + +## How to Expose a Hook in a Workflow? + +To expose a hook in your workflow, use `createHook` from the Workflows SDK. + +For example: + +```ts title="src/workflows/my-workflow/index.ts" highlights={hookHighlights} import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - createBrandWorkflow, -} from "../../../workflows/create-brand" + createStep, + createHook, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { createProductStep } from "./steps/create-product" -type PostAdminCreateBrandType = { - name: string -} +export const myWorkflow = createWorkflow( + "my-workflow", + function (input) { + const product = createProductStep(input) + const productCreatedHook = createHook( + "productCreated", + { productId: product.id } + ) -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const { result } = await createBrandWorkflow(req.scope) - .run({ - input: req.validatedBody, + return new WorkflowResponse(product, { + hooks: [productCreatedHook], }) - - res.json({ brand: result }) -} + } +) ``` -You export a route handler function with its name (`POST`) being the HTTP method of the API route you're exposing. +The `createHook` function accepts two parameters: -The function receives two parameters: a `MedusaRequest` object to access request details, and `MedusaResponse` object to return or manipulate the response. The `MedusaRequest` object's `scope` property is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) that holds framework tools and custom and core modules' services. +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. -`MedusaRequest` accepts the request body's type as a type argument. +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. -In the API route's handler, you execute the `createBrandWorkflow` by invoking it and passing the Medusa container `req.scope` as a parameter, then invoking its `run` method. You pass the workflow's input in the `input` property of the `run` method's parameter. You pass the request body's parameters using the `validatedBody` property of `MedusaRequest`. +### How to Consume the Hook? -You return a JSON response with the created brand using the `res.json` method. +To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content: -*** +```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights} +import { myWorkflow } from "../my-workflow" -## 2. Create Validation Schema +myWorkflow.hooks.productCreated( + async ({ productId }, { container }) => { + // TODO perform an action + } +) +``` -The API route you created accepts the brand's name in the request body. So, you'll create a schema used to validate incoming request body parameters. +The hook is available on the workflow's `hooks` property using its name `productCreated`. -Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. +You invoke the hook, passing a step function (the hook handler) as a parameter. -Learn more about API route validation in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). -You create a validation schema in a TypeScript or JavaScript file under a sub-directory of the `src/api` directory. So, create the file `src/api/admin/brands/validators.ts` with the following content: +# Workflow Constraints -![Directory structure of Medusa application after adding validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869806/Medusa%20Book/brand-route-dir-overview-1_yfyjss.jpg) +This chapter lists constraints of defining a workflow or its steps. -```ts title="src/api/admin/brands/validators.ts" -import { z } from "zod" +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. -export const PostAdminCreateBrand = z.object({ - name: z.string(), -}) -``` +This creates restrictions related to variable manipulations, using if-conditions, and other constraints. This chapter lists these constraints and provides their alternatives. -You export a validation schema that expects in the request body an object having a `name` property whose value is a string. +## Workflow Constraints -You can then replace `PostAdminCreateBrandType` in `src/api/admin/brands/route.ts` with the following: +### No Async Functions -```ts title="src/api/admin/brands/route.ts" -// ... -import { z } from "zod" -import { PostAdminCreateBrand } from "./validators" +The function passed to `createWorkflow` can’t be an async function: -type PostAdminCreateBrandType = z.infer +```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) { + // ... +}) ``` -*** - -## 3. Add Validation Middleware +### No Direct Variable Manipulation -A middleware is a function executed before the route handler when a request is sent to an API Route. It's useful to guard API routes, parse custom request body types, and apply validation on an API route. +You can’t directly manipulate variables within the workflow's constructor function. -Learn more about middlewares in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). +Learn more about why you can't manipulate variables [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) -Medusa provides a `validateAndTransformBody` middleware that accepts a Zod validation schema and returns a response error if a request is sent with body parameters that don't satisfy the validation schema. +Instead, use `transform` from the Workflows SDK: -Middlewares are defined in the special file `src/api/middlewares.ts`. So, to add the validation middleware on the API route you created in the previous step, create the file `src/api/middlewares.ts` with the following content: +```ts highlights={highlights} +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const str1 = step1(input) + const str2 = step2(input) -![Directory structure of the Medusa application after adding the middleware](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869977/Medusa%20Book/brand-route-dir-overview-3_kcx511.jpg) + return new WorkflowResponse({ + message: `${str1}${str2}`, + }) +}) -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - validateAndTransformBody, -} from "@medusajs/framework/http" -import { PostAdminCreateBrand } from "./admin/brands/validators" +// Do +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const str1 = step1(input) + const str2 = step2(input) -export default defineMiddlewares({ - routes: [ + const result = transform( { - matcher: "/admin/brands", - method: "POST", - middlewares: [ - validateAndTransformBody(PostAdminCreateBrand), - ], + str1, + str2, }, - ], + (input) => ({ + message: `${input.str1}${input.str2}`, + }) + ) + + return new WorkflowResponse(result) }) ``` -You define the middlewares using the `defineMiddlewares` function and export its returned value. The function accepts an object having a `routes` property, which is an array of middleware objects. +### Create Dates in transform -In the middleware object, you define three properties: +When you use `new Date()` in a workflow's constructor function, the date is evaluated when Medusa creates the internal representation of the workflow, not during execution. -- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. You pass the create brand's route `/admin/brand`. -- `method`: The HTTP method to restrict the middleware to, which is `POST`. -- `middlewares`: An array of middlewares to apply on the route. You pass the `validateAndTransformBody` middleware, passing it the Zod schema you created earlier. +Instead, create the date using `transform`. -The Medusa application will now validate the body parameters of `POST` requests sent to `/admin/brands` to ensure they match the Zod validation schema. If not, an error is returned in the response specifying the issues to fix in the request body. +Learn more about how Medusa creates an internal representation of a workflow [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). -*** +For example: -## Test API Route +```ts highlights={dateHighlights} +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const today = new Date() -To test out the API route, start the Medusa application with the following command: + return new WorkflowResponse({ + today, + }) +}) -```bash npm2yarn -npm run dev +// Do +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const today = transform({}, () => new Date()) + + return new WorkflowResponse({ + today, + }) +}) ``` -Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. +### No If Conditions -So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route: +You can't use if-conditions in a workflow. -```bash -curl -X POST 'http://localhost:9000/auth/user/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "admin@medusa-test.com", - "password": "supersecret" -}' +Learn more about why you can't use if-conditions [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows/index.html.md) + +Instead, use when-then from the 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 + }) +}) ``` -Make sure to replace the email and password with your admin user's credentials. +You can also pair multiple `when-then` blocks to implement an `if-else` condition as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). -Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). +### No Conditional Operators -Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: +You can't use conditional operators in a workflow, such as `??` or `||`. -```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. You did that by: - -1. Creating a module that defines and manages a `brand` table in the database. -2. Creating a workflow that uses the module's 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. +Learn more about why you can't use conditional operators [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows/index.html.md) -*** +Instead, use `transform` to store the desired value in a variable. -## Next Steps: Associate Brand with Product +### Logical Or (||) Alternative -Now that you have brands in your Medusa application, you want to associate a brand with a product, which is defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md). +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const message = input.message || "Hello" +}) -In the next chapters, you'll learn how to build associations between data models defined in different modules. +// 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" + ) +}) +``` -# Guide: Implement Brand Module +### Nullish Coalescing (??) Alternative -In this chapter, you'll build a Brand Module that adds a `brand` table to the database and provides data-management features for it. +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const message = input.message ?? "Hello" +}) -A module is a reusable package of functionalities related to a single domain or integration. Medusa comes with multiple pre-built modules for core commerce needs, such as the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) that holds the data models and business logic for cart operations. +// Do +// other imports... +import { transform } from "@medusajs/framework/workflows-sdk" -In a module, you create data models and business logic to manage them. In the next chapters, you'll see how you use the module to build commerce features. +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const message = transform( + { + input, + }, + (data) => data.input.message ?? "hello" + ) +}) +``` -Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +### Double Not (!!) Alternative -## 1. Create Module Directory +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + step1({ + isActive: !!input.is_active, + }) +}) -Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/brand` that will hold the Brand Module's files. +// Do +// other imports... +import { transform } from "@medusajs/framework/workflows-sdk" -![Directory structure in Medusa project after adding the brand directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868844/Medusa%20Book/brand-dir-overview-1_hxwvgx.jpg) +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const isActive = transform( + { + input, + }, + (data) => !!data.input.is_active + ) + + step1({ + isActive, + }) +}) +``` -*** +### Ternary Alternative -## 2. Create Data Model +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + step1({ + message: input.is_active ? "active" : "inactive", + }) +}) -A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. +// Do +// other imports... +import { transform } from "@medusajs/framework/workflows-sdk" -Learn more about data models in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#1-create-data-model/index.html.md). +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const message = transform( + { + input, + }, + (data) => { + return data.input.is_active ? "active" : "inactive" + } + ) + + step1({ + message, + }) +}) +``` -You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, 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: +### Optional Chaining (?.) Alternative -![Directory structure in module after adding the brand data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868920/Medusa%20Book/brand-dir-overview-2_lexhdl.jpg) +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + step1({ + name: input.customer?.name, + }) +}) -```ts title="src/modules/brand/models/brand.ts" -import { model } from "@medusajs/framework/utils" +// Do +// other imports... +import { transform } from "@medusajs/framework/workflows-sdk" -export const Brand = model.define("brand", { - id: model.id().primaryKey(), - name: model.text(), +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const name = transform( + { + input, + }, + (data) => data.input.customer?.name + ) + + step1({ + name, + }) }) ``` -You create a `Brand` data model which has an `id` primary key property, and a `name` text property. +*** -You define the data model using the `define` method of the DML. It accepts two parameters: +## Step Constraints -1. The first one is the name of the data model's table in the database. Use snake-case names. -2. The second is an object, which is the data model's schema. +### Returned Values -Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/property-types/index.html.md). +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. -## 3. Create Module Service +```ts +// Don't +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" -You perform database operations on your data models in a service, which is a class exported by the module and acts like an interface to its functionalities. +const step1 = createStep( + "step-1", + (input, { container }) => { + const myMap = new Map() -In this step, you'll create the Brand Module's service that provides methods to manage the `Brand` data model. In the next chapters, you'll use this service when exposing custom features that involve managing brands. + // ... -Learn more about services in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#2-create-service/index.html.md). + return new StepResponse({ + myMap, + }) + } +) -You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, create the file `src/modules/brand/service.ts` with the following content: +// Do +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" -![Directory structure in module after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868984/Medusa%20Book/brand-dir-overview-3_jo7baj.jpg) +const step1 = createStep( + "step-1", + (input, { container }) => { + const myObj: Record = {} -```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, -}) { + return new StepResponse({ + myObj, + }) + } +) +``` -} -export default BrandModuleService -``` +# Compensation Function -The `BrandModuleService` extends a class returned by `MedusaService` from the Modules SDK. This function generates a class with data-management methods for your module's data models. +In this chapter, you'll learn what a compensation function is and how to add it to a step. -The `MedusaService` function receives an object of the module's data models as a parameter, and generates methods to manage those data models. So, the `BrandModuleService` now has methods like `createBrands` and `retrieveBrand` to manage the `Brand` data model. +## What is a Compensation Function -You'll use these methods in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). +A compensation function rolls back or undoes changes made by a step when an error occurs in the workflow. -Find a reference of all generated methods in [this guide](https://docs.medusajs.com/resources/service-factory-reference/index.html.md). +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. *** -## 4. Export Module Definition +## How to add a Compensation Function? -A module must export a definition that tells Medusa the name of the module and its main service. This definition is exported in an `index.ts` file at the module's root directory. +A compensation function is passed as a second parameter to the `createStep` function. -So, to export the Brand Module's definition, create the file `src/modules/brand/index.ts` with the following content: +For example, create the file `src/workflows/hello-world.ts` with the following content: -![Directory structure in module after adding the definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869045/Medusa%20Book/brand-dir-overview-4_nf8ymw.jpg) +```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" -```ts title="src/modules/brand/index.ts" -import { Module } from "@medusajs/framework/utils" -import BrandModuleService from "./service" +const step1 = createStep( + "step-1", + async () => { + const message = `Hello from step one!` -export const BRAND_MODULE = "brand" + console.log(message) -export default Module(BRAND_MODULE, { - service: BrandModuleService, -}) + return new StepResponse(message) + }, + async () => { + console.log("Oops! Rolling back my changes...") + } +) ``` -You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: +Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow. -1. The module's name (`brand`). You'll use this name when you use this module in other customizations. -2. An object with a required property `service` indicating the module's main service. +*** -You export `BRAND_MODULE` to reference the module's name more reliably in other customizations. +## Test the Compensation Function -*** +Create a step in the same `src/workflows/hello-world.ts` file that throws an error: -## 5. Add Module to Medusa's Configurations +```ts title="src/workflows/hello-world.ts" +const step2 = createStep( + "step-2", + async () => { + throw new Error("Throwing an error...") + } +) +``` -To start using your module, you must add it to Medusa's configurations in `medusa-config.ts`. +Then, create a workflow that uses the steps: -The object passed to `defineConfig` in `medusa-config.ts` accepts a `modules` property, whose value is an array of modules to add to the application. So, add the following in `medusa-config.ts`: +```ts title="src/workflows/hello-world.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +// other imports... -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/brand", - }, - ], -}) -``` +// steps... -The Brand Module is now added to your Medusa application. You'll start using it in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str1 = step1() + step2() -*** + return new WorkflowResponse({ + message: str1, + }) +}) -## 6. Generate and Run Migrations +export default myWorkflow +``` -A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations ensure that your module is re-usable and removes friction when working in a team, making it easy to reflect changes across team members' databases. +Finally, execute the workflow from an API route: -Learn more about migrations in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#5-generate-migrations/index.html.md). +```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" -[Medusa's CLI tool](https://docs.medusajs.com/resources/medusa-cli/index.html.md) allows you to generate migration files for your module, then run those migrations to reflect the changes in the database. So, run the following commands in your Medusa application's directory: +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await myWorkflow(req.scope) + .run() -```bash -npx medusa db:generate brand -npx medusa db:migrate + res.send(result) +} ``` -The `db:generate` command accepts as an argument the name of the module to generate the migrations for, and the `db:migrate` command runs all migrations that haven't been run yet in the Medusa application. +Run the Medusa application and send a `GET` request to `/workflow`: -*** +```bash +curl http://localhost:9000/workflow +``` -## Next Step: Create Brand Workflow +In the console, you'll see: -The Brand Module now creates a `brand` table in the database and provides a class to manage its records. +- `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. -In the next chapter, you'll implement the functionality to create a brand in a workflow. You'll then use that workflow in a later chapter to expose an endpoint that allows admin users to create a brand. +*** +## Pass Input to Compensation Function -# Guide: Sync Brands from Medusa to CMS +If a step creates a record, the compensation function must receive the ID of the record to remove it. -In the [previous chapter](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows. +To pass input to the compensation function, pass a second parameter in the `StepResponse` returned by the step. -In another previous chapter, you [added a workflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well. +For example: -Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system. +```ts highlights={inputHighlights} +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" -Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). +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 chapter, you'll modify the `createBrandWorkflow` you created before to emit a custom event that indicates a brand was created. Then, you'll listen to that event in a subscriber to sync the brand to the third-party CMS. You'll implement the sync logic within a workflow that you execute in the subscriber. +In this example, the step passes an object as a second parameter to `StepResponse`. -### Prerequisites +The compensation function receives the object and uses its `message` property to log a message. -- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) -- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) +*** -## 1. Emit Event in createBrandWorkflow +## Resolve Resources from the Medusa Container -Since syncing the brand to the third-party system isn't integral to creating a brand, you'll emit a custom event indicating that a brand was created. +The compensation function receives an object second parameter. The object has a `container` property that you use to resolve resources from the Medusa container. -Medusa provides an `emitEventStep` that allows you to emit an event in your workflows. So, in the `createBrandWorkflow` defined in `src/workflows/create-brand.ts`, use the `emitEventStep` helper step after the `createBrandStep`: +For example: -```ts title="src/workflows/create-brand.ts" highlights={eventHighlights} -// other imports... +```ts import { - emitEventStep, -} from "@medusajs/medusa/core-flows" - -// ... - -export const createBrandWorkflow = createWorkflow( - "create-brand", - (input: CreateBrandInput) => { - // ... + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" - emitEventStep({ - eventName: "brand.created", - data: { - id: brand.id, - }, - }) +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 + ) - return new WorkflowResponse(brand) + logger.info(message) } ) ``` -The `emitEventStep` accepts an object parameter having two properties: - -- `eventName`: The name of the event to emit. You'll use this name later to listen to the event in a subscriber. -- `data`: The data payload to emit with the event. This data is passed to subscribers that listen to the event. You add the brand's ID to the data payload, informing the subscribers which brand was created. +In this example, you use the `container` property in the second object parameter of the compensation function to resolve the logger. -You'll learn how to handle this event in a later step. +You then use the logger to log a message. *** -## 2. Create Sync to Third-Party System Workflow - -The subscriber that will listen to the `brand.created` event will sync the created brand to the third-party CMS. So, you'll implement the syncing logic in a workflow, then execute the workflow in the subscriber. - -Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. - -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). - -You'll create a `syncBrandToSystemWorkflow` that has two steps: - -- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll use this to retrieve the brand's details using its ID. -- `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS. - -### syncBrandToCmsStep +## Handle Errors in Loops -To implement the step that syncs the brand to the CMS, create the file `src/workflows/sync-brands-to-cms.ts` with the following content: +This feature is only available after [Medusa v2.0.5](https://github.com/medusajs/medusa/releases/tag/v2.0.5). -![Directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493547/Medusa%20Book/cms-dir-overview-4_u5t0ug.jpg) +Consider you have a module that integrates a third-party ERP system, and you're creating a workflow that deletes items in that ERP. You may have the following step: -```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import { InferTypeOf } from "@medusajs/framework/types" -import { Brand } from "../modules/brand/models/brand" -import { CMS_MODULE } from "../modules/cms" -import CmsModuleService from "../modules/cms/service" +```ts +// other imports... +import { promiseAll } from "@medusajs/framework/utils" -type SyncBrandToCmsStepInput = { - brand: InferTypeOf +type StepInput = { + ids: string[] } -const syncBrandToCmsStep = createStep( - "sync-brand-to-cms", - async ({ brand }: SyncBrandToCmsStepInput, { container }) => { - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) +const step1 = createStep( + "step-1", + async ({ ids }: StepInput, { container }) => { + const erpModuleService = container.resolve( + ERP_MODULE + ) + const prevData: unknown[] = [] - await cmsModuleService.createBrand(brand) + await promiseAll( + ids.map(async (id) => { + const data = await erpModuleService.retrieve(id) - return new StepResponse(null, brand.id) - }, - async (id, { container }) => { - if (!id) { - return - } + await erpModuleService.delete(id) - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) + prevData.push(id) + }) + ) - await cmsModuleService.deleteBrand(id) + return new StepResponse(ids, prevData) } ) ``` -You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) and use its `createBrand` method. This method will create the brand in the third-party CMS. +In the step, you loop over the IDs to retrieve the item's data, store them in a `prevData` variable, then delete them using the ERP Module's service. You then pass the `prevData` variable to the compensation function. -You also pass the brand's ID to the step's compensation function. In this function, you delete the brand in the third-party CMS if an error occurs during the workflow's execution. - -Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). - -### Create Workflow - -You can now create the workflow that uses the above step. Add the workflow to the same `src/workflows/sync-brands-to-cms.ts` file: +However, if an error occurs in the loop, the `prevData` variable won't be passed to the compensation function as the execution never reached the return statement. -```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncWorkflowHighlights} -// other imports... -import { - // ... - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +To handle errors in the loop so that the compensation function receives the last version of `prevData` before the error occurred, you wrap the loop in a try-catch block. Then, in the catch block, you invoke and return the `StepResponse.permanentFailure` function: -// ... +```ts highlights={highlights} +try { + await promiseAll( + ids.map(async (id) => { + const data = await erpModuleService.retrieve(id) -type SyncBrandToCmsWorkflowInput = { - id: string -} + await erpModuleService.delete(id) -export const syncBrandToCmsWorkflow = createWorkflow( - "sync-brand-to-cms", - (input: SyncBrandToCmsWorkflowInput) => { - // @ts-ignore - const { data: brands } = useQueryGraphStep({ - entity: "brand", - fields: ["*"], - filters: { - id: input.id, - }, - options: { - throwIfKeyNotFound: true, - }, + prevData.push(id) }) - - syncBrandToCmsStep({ - brand: brands[0], - } as SyncBrandToCmsStepInput) - - return new WorkflowResponse({}) - } -) + ) +} catch (e) { + return StepResponse.permanentFailure( + `An error occurred: ${e}`, + prevData + ) +} ``` -You create a `syncBrandToCmsWorkflow` that accepts the brand's ID as input. The workflow has the following steps: - -- `useQueryGraphStep`: Retrieve the brand's details using Query. You pass the brand's ID as a filter, and set the `throwIfKeyNotFound` option to true so that the step throws an error if a brand with the specified ID doesn't exist. -- `syncBrandToCmsStep`: Create the brand in the third-party CMS. +The `StepResponse.permanentFailure` fails the step and its workflow, triggering current and previous steps' compensation functions. The `permanentFailure` function accepts as a first parameter the error message, which is saved in the workflow's error details, and as a second parameter the data to pass to the compensation function. -You'll execute this workflow in the subscriber next. +So, if an error occurs during the loop, the compensation function will still receive the `prevData` variable to undo the changes made before the step failed. -Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). -*** +# Execute Another Workflow -## 3. Handle brand.created Event +In this chapter, you'll learn how to execute a workflow in another. -You now have a workflow with the logic to sync a brand to the CMS. You need to execute this workflow whenever the `brand.created` event is emitted. So, you'll create a subscriber that listens to and handle the event. +## Execute in a Workflow -Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/brand-created.ts` with the following content: +To execute a workflow in another, use the `runAsStep` method that every workflow has. -![Directory structure of the Medusa application after adding the subscriber](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493774/Medusa%20Book/cms-dir-overview-5_iqqwvg.jpg) +For example: -```ts title="src/subscribers/brand-created.ts" highlights={subscriberHighlights} -import type { - SubscriberConfig, - SubscriberArgs, -} from "@medusajs/framework" -import { syncBrandToCmsWorkflow } from "../workflows/sync-brands-to-cms" +```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports" +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" -export default async function brandCreatedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await syncBrandToCmsWorkflow(container).run({ - input: data, - }) -} +const workflow = createWorkflow( + "hello-world", + async (input) => { + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + // ... + ], + }, + }) -export const config: SubscriberConfig = { - event: "brand.created", -} + // ... + } +) ``` -A subscriber file must export: +Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. -- The asynchronous function that's executed when the event is emitted. This must be the file's default export. -- An object that holds the subscriber's configurations. It has an `event` property that indicates the name of the event that the subscriber is listening to. +The object has an `input` property to pass input to the workflow. -The subscriber function accepts an object parameter that has two properties: +*** -- `event`: An object of event details. Its `data` property holds the event's data payload, which is the brand's ID. -- `container`: The Medusa container used to resolve framework and commerce tools. +## Preparing Input Data -In the function, you execute the `syncBrandToCmsWorkflow`, passing it the data payload as an input. So, everytime a brand is created, Medusa will execute this function, which in turn executes the workflow to sync the brand to the CMS. +If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. -Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). +Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). -*** +For example: -## Test it Out +```ts highlights={transformHighlights} collapsibleLines="1-12" +import { + createWorkflow, + transform, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" -To test the subscriber and workflow out, you'll use the [Create Brand API route](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md) you created in a previous chapter. +type WorkflowInput = { + title: string +} -First, start the Medusa application: +const workflow = createWorkflow( + "hello-product", + async (input: WorkflowInput) => { + const createProductsData = transform({ + input, + }, (data) => [ + { + title: `Hello ${data.input.title}`, + }, + ]) -```bash npm2yarn -npm run dev + const products = createProductsWorkflow.runAsStep({ + input: { + products: createProductsData, + }, + }) + + // ... + } +) ``` -Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route: +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`. -```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 admin user's credentials. +## Run Workflow Conditionally -Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). +To run a workflow in another based on a condition, use when-then from the Workflows SDK. -Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: +Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). -```bash -curl -X POST 'http://localhost:9000/admin/brands' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data '{ - "name": "Acme" -}' -``` +For example: -This request returns the created brand. 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: +```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" -```plain -info: Processing brand.created which has 1 subscribers -http: POST /admin/brands ← - (200) - 16.418 ms -info: Sending a POST request to /brands. -info: Request Data: { - "id": "01JEDWENYD361P664WRQPMC3J8", - "name": "Acme", - "created_at": "2024-12-06T11:42:32.909Z", - "updated_at": "2024-12-06T11:42:32.909Z", - "deleted_at": null +type WorkflowInput = { + product?: CreateProductWorkflowInputDTO + should_create?: boolean } -info: API Key: "123" -``` - -*** -## Next Chapter: Sync Brand from Third-Party CMS to Medusa +const workflow = createWorkflow( + "hello-product", + async (input: WorkflowInput) => { + const product = when(input, ({ should_create }) => should_create) + .then(() => { + return createProductsWorkflow.runAsStep({ + input: { + products: [input.product], + }, + }) + }) + } +) +``` -You can also automate syncing data from a third-party system to Medusa at a regular interval. In the next chapter, you'll learn how to sync brands from the third-party CMS to Medusa once a day. +In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. -# Guide: Create Brand Workflow +# Conditions in Workflows with When-Then -This chapter builds on the work from the [previous chapter](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) where you created a Brand Module. +In this chapter, you'll learn how to execute an action based on a condition in a workflow using when-then from the Workflows SDK. -After adding custom modules to your application, you build commerce features around them using workflows. A workflow is a series of queries and actions, called steps, that complete a task spanning across modules. You construct a workflow similar to a regular function, but it's a special function that allows you to define roll-back logic, retry configurations, and more advanced features. +## Why If-Conditions Aren't Allowed in Workflows? -The workflow you'll create in this chapter will use the Brand Module's service to implement the feature of creating a brand. In the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll expose an API route that allows admin users to create a brand, and you'll use this workflow in the route's implementation. +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. -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). +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. -### Prerequisites +Instead, use when-then from the Workflows SDK. It allows you to perform steps in a workflow only if a condition that you specify is satisfied. -- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code. *** -## 1. Create createBrandStep - -A workflow consists of a series of steps, each step created in a TypeScript or JavaScript file under the `src/workflows` directory. A step is defined using `createStep` from the Workflows SDK +## How to use When-Then? -The workflow you're creating in this guide has one step to create the brand. So, create the file `src/workflows/create-brand.ts` with the following content: +The Workflows SDK provides a `when` function that is used to check whether a condition is true. You chain a `then` function to `when` that specifies the steps to execute if the condition in `when` is satisfied. -![Directory structure in the Medusa project after adding the file for createBrandStep](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869184/Medusa%20Book/brand-workflow-dir-overview-1_fjvf5j.jpg) +For example: -```ts title="src/workflows/create-brand.ts" -import { - createStep, - StepResponse, +```ts highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + when, } from "@medusajs/framework/workflows-sdk" -import { BRAND_MODULE } from "../modules/brand" -import BrandModuleService from "../modules/brand/service" +// step imports... -export type CreateBrandStepInput = { - name: string -} +const workflow = createWorkflow( + "workflow", + function (input: { + is_active: boolean + }) { -export const createBrandStep = createStep( - "create-brand-step", - async (input: CreateBrandStepInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) + const result = when( + input, + (input) => { + return input.is_active + } + ).then(() => { + const stepResult = isActiveStep() + return stepResult + }) - const brand = await brandModuleService.createBrands(input) + // executed without condition + const anotherStepResult = anotherStep(result) - return new StepResponse(brand, brand.id) + return new WorkflowResponse( + anotherStepResult + ) } ) ``` -You create a `createBrandStep` using the `createStep` function. It accepts the step's unique name as a first parameter, and the step's function as a second parameter. +In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`. -The step function receives two parameters: input passed to the step when it's invoked, and an object of general context and configurations. This object has a `container` property, which is the Medusa container. +### When Parameters -The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) is a registry of framework and commerce tools accessible in your customizations, such as a workflow's step. The Medusa application registers the services of core and custom modules in the container, allowing you to resolve and use them. +`when` accepts the following parameters: -So, In the step function, you use the Medusa container to resolve the Brand Module's service and use its generated `createBrands` method, which accepts an object of brands to create. +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`. -Learn more about the generated `create` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/create/index.html.md). +### Then Parameters -A step must return an instance of `StepResponse`. Its first parameter is the data returned by the step, and the second is the data passed to the compensation function, which you'll learn about next. +To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. -### Add Compensation Function to Step +The callback function is only executed if `when`'s second parameter function returns a `true` value. -You define for each step a compensation function that's executed when an error occurs in the workflow. The compensation function defines the logic to roll-back the changes made by the step. This ensures your data remains consistent if an error occurs, which is especially useful when you integrate third-party services. +*** -Learn more about the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). +## Implementing If-Else with When-Then -To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`: +when-then doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow. -```ts title="src/workflows/create-brand.ts" -export const createBrandStep = createStep( - // ... - async (id: string, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +For example: - await brandModuleService.deleteBrands(id) - } -) -``` +```ts highlights={ifElseHighlights} +const workflow = createWorkflow( + "workflow", + function (input: { + is_active: boolean + }) { -The compensation function's first parameter is the brand's ID which you passed as a second parameter to the step function's returned `StepResponse`. It also accepts a context object with a `container` property as a second parameter, similar to the step function. + const isActiveResult = when( + input, + (input) => { + return input.is_active + } + ).then(() => { + return isActiveStep() + }) -In the compensation function, you resolve the Brand Module's service from the Medusa container, then use its generated `deleteBrands` method to delete the brand created by the step. This method accepts the ID of the brand to delete. + const notIsActiveResult = when( + input, + (input) => { + return !input.is_active + } + ).then(() => { + return notIsActiveStep() + }) -Learn more about the generated `delete` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/delete/index.html.md). + // ... + } +) +``` -So, if an error occurs during the workflow's execution, the brand that was created by the step is deleted to maintain data consistency. +In the above workflow, you use two `when-then` blocks. The first one performs a step if `input.is_active` is `true`, and the second performs a step if `input.is_active` is `false`, acting as an else condition. *** -## 2. Create createBrandWorkflow +## Specify Name for When-Then -You can now create the workflow that runs the `createBrandStep`. A workflow is created in a TypeScript or JavaScript file under the `src/workflows` directory. In the file, you use `createWorkflow` from the Workflows SDK to create the workflow. +Internally, `when-then` blocks have a unique name similar to a step. When you return a step's result in a `when-then` block, the block's name is derived from the step's name. For example: -Add the following content in the same `src/workflows/create-brand.ts` file: +```ts +const isActiveResult = when( + input, + (input) => { + return input.is_active + } +).then(() => { + return isActiveStep() +}) +``` -```ts title="src/workflows/create-brand.ts" -// other imports... -import { - // ... - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name. -// ... +However, if you need to return in your `when-then` block something other than a step's result, you need to specify a unique step name for that block. Otherwise, Medusa will generate a random name for it which can cause unexpected errors in production. -type CreateBrandWorkflowInput = { - name: string -} +You pass a name for `when-then` as a first parameter of `when`, whose signature can accept three parameters in this case. For example: -export const createBrandWorkflow = createWorkflow( - "create-brand", - (input: CreateBrandWorkflowInput) => { - const brand = createBrandStep(input) +```ts highlights={nameHighlights} +const { isActive } = when( + "check-is-active", + input, + (input) => { + return input.is_active + } +).then(() => { + const isActive = isActiveStep() - return new WorkflowResponse(brand) + return { + isActive, } -) +}) ``` -You create the `createBrandWorkflow` using the `createWorkflow` function. This function accepts two parameters: the workflow's unique name, and the workflow's constructor function holding the workflow's implementation. +Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters: -The constructor function accepts the workflow's input as a parameter. In the function, you invoke the `createBrandStep` you created in the previous step to create a brand. +1. A unique name to be assigned to the `when-then` block. +2. Either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. +3. A function that returns a boolean indicating whether to execute the action in `then`. -A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor. +The second and third parameters are the same as the parameters you previously passed to `when`. -*** -## Next Steps: Expose Create Brand API Route +# Long-Running Workflows -You now have a `createBrandWorkflow` that you can execute to create a brand. +In this chapter, you’ll learn what a long-running workflow is and how to configure it. -In the next chapter, you'll add an API route that allows admin users to create a brand. You'll learn how to create the API route, and execute in it the workflow you implemented in this chapter. +## What is a Long-Running Workflow? +When you execute a workflow, you wait until the workflow finishes execution to receive the output. -# Guide: Schedule Syncing Brands from CMS +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. -In the previous chapters, you've [integrated a third-party CMS](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) and implemented the logic to [sync created brands](https://docs.medusajs.com/learn/customization/integrate-systems/handle-event/index.html.md) from Medusa to the CMS. +### Why use Long-Running Workflows? -However, when you integrate a third-party system, you want the data to be in sync between the Medusa application and the system. One way to do so is by automatically syncing the data once a day. +Long-running workflows are useful if: -You can create an action to be automatically executed at a specified interval using scheduled jobs. A scheduled job is an asynchronous function with a specified schedule of when the Medusa application should run it. Scheduled jobs are useful to automate repeated tasks. +- 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. -Learn more about scheduled jobs in [this chapter](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). +*** -In this chapter, you'll create a scheduled job that triggers syncing the brands from the third-party CMS to Medusa once a day. You'll implement the syncing logic in a workflow, and execute that workflow in the scheduled job. +## Configure Long-Running Workflows -### Prerequisites +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. -- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) +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" -## 1. Implement Syncing Workflow +const step1 = createStep("step-1", async () => { + return new StepResponse({}) +}) -You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job. +const step2 = createStep( + { + name: "step-2", + async: true, + }, + async () => { + console.log("Waiting to be successful...") + } +) -Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. +const step3 = createStep("step-3", async () => { + return new StepResponse("Finished three steps") +}) -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). +const myWorkflow = createWorkflow( + "hello-world", + function () { + step1() + step2() + const message = step3() -This workflow will have three steps: + return new WorkflowResponse({ + message, + }) +}) -1. `retrieveBrandsFromCmsStep` to retrieve the brands from the CMS. -2. `createBrandsStep` to create the brands retrieved in the first step that don't exist in Medusa. -3. `updateBrandsStep` to update the brands retrieved in the first step that exist in Medusa. +export default myWorkflow +``` -### retrieveBrandsFromCmsStep +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. -To create the step that retrieves the brands from the third-party CMS, create the file `src/workflows/sync-brands-from-cms.ts` with the following content: +So, when you execute the `hello-world` workflow, it continues its execution in the background once it reaches the second step. -![Directory structure of the Medusa application after creating the file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494196/Medusa%20Book/cms-dir-overview-6_z1omsi.jpg) +A workflow is also considered long-running if one of its steps has their `retryInterval` option set as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md). -```ts title="src/workflows/sync-brands-from-cms.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import CmsModuleService from "../modules/cms/service" -import { CMS_MODULE } from "../modules/cms" +*** -const retrieveBrandsFromCmsStep = createStep( - "retrieve-brands-from-cms", - async (_, { container }) => { - const cmsModuleService: CmsModuleService = container.resolve( - CMS_MODULE - ) +## Change Step Status - const brands = await cmsModuleService.retrieveBrands() +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. - return new StepResponse(brands) - } -) -``` +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. -You create a `retrieveBrandsFromCmsStep` that resolves the CMS Module's service and uses its `retrieveBrands` method to retrieve the brands in the CMS. You return those brands in the step's response. +### Retrieve Transaction ID -### createBrandsStep +Before changing the status of a workflow execution's async step, you must have the execution's transaction ID. -The brands retrieved in the first step may have brands that don't exist in Medusa. So, you'll create a step that creates those brands. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: +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 title="src/workflows/sync-brands-from-cms.ts" highlights={createBrandsHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" -// other imports... -import BrandModuleService from "../modules/brand/service" -import { BRAND_MODULE } from "../modules/brand" +```ts +const { transaction } = await myWorkflow(req.scope) + .run() -// ... +// use transaction.transactionId later +``` -type CreateBrand = { - name: string -} +### Change Step Status to Successful -type CreateBrandsInput = { - brands: CreateBrand[] -} +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. -export const createBrandsStep = createStep( - "create-brands-step", - async (input: CreateBrandsInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +For example, consider the following step: - const brands = await brandModuleService.createBrands(input.brands) +```ts highlights={successStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + Modules, + TransactionHandlerType, +} from "@medusajs/framework/utils" +import { + StepResponse, + createStep, +} from "@medusajs/framework/workflows-sdk" - return new StepResponse(brands, brands) - }, - async (brands, { container }) => { - if (!brands) { - return - } +type SetStepSuccessStepInput = { + transactionId: string +}; - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE +export const setStepSuccessStep = createStep( + "set-step-success-step", + async function ( + { transactionId }: SetStepSuccessStepInput, + { container } + ) { + const workflowEngineService = container.resolve( + Modules.WORKFLOW_ENGINE ) - await brandModuleService.deleteBrands(brands.map((brand) => brand.id)) + await workflowEngineService.setStepSuccess({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId, + stepId: "step-2", + workflowId: "hello-world", + }, + stepResponse: new StepResponse("Done!"), + options: { + container, + }, + }) } ) ``` -The `createBrandsStep` accepts the brands to create as an input. It resolves the [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)'s service and uses the generated `createBrands` method to create the brands. +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 step passes the created brands to the compensation function, which deletes those brands if an error occurs during the workflow's execution. +The `setStepSuccess` method of the workflow engine's main service accepts as a parameter an object having the following properties: -Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). +- idempotencyKey: (\`object\`) The details of the workflow execution. -### Update Brands Step + - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. -The brands retrieved in the first step may also have brands that exist in Medusa. So, you'll create a step that updates their details to match that of the CMS. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: + - transactionId: (\`string\`) The ID of the workflow execution's transaction. -```ts title="src/workflows/sync-brands-from-cms.ts" highlights={updateBrandsHighlights} -// ... + - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step. -type UpdateBrand = { - id: string - name: string -} + - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow. +- stepResponse: (\`StepResponse\`) 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. +- options: (\`Record\\`) Options to pass to the step. -type UpdateBrandsInput = { - brands: UpdateBrand[] -} + - container: (\`MedusaContainer\`) An instance of the Medusa Container -export const updateBrandsStep = createStep( - "update-brands-step", - async ({ brands }: UpdateBrandsInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +### Change Step Status to Failed - const prevUpdatedBrands = await brandModuleService.listBrands({ - id: brands.map((brand) => brand.id), - }) +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`. - const updatedBrands = await brandModuleService.updateBrands(brands) +After changing the async step's status to failed, the workflow execution fails and the compensation functions of previous steps are executed. - return new StepResponse(updatedBrands, prevUpdatedBrands) - }, - async (prevUpdatedBrands, { container }) => { - if (!prevUpdatedBrands) { - return - } +For example: - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE +```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 brandModuleService.updateBrands(prevUpdatedBrands) + await workflowEngineService.setStepFailure({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId, + stepId: "step-2", + workflowId: "hello-world", + }, + stepResponse: new StepResponse("Failed!"), + options: { + container, + }, + }) } ) ``` -The `updateBrandsStep` receives the brands to update in Medusa. In the step, you retrieve the brand's details in Medusa before the update to pass them to the compensation function. You then update the brands using the Brand Module's `updateBrands` generated method. - -In the compensation function, which receives the brand's old data, you revert the update using the same `updateBrands` method. - -### Create Workflow - -Finally, you'll create the workflow that uses the above steps to sync the brands from the CMS to Medusa. Add to the same `src/workflows/sync-brands-from-cms.ts` file the following: +You use this step in another workflow that changes the status of an async step in a long-running workflow's execution to failed. -```ts title="src/workflows/sync-brands-from-cms.ts" -// other imports... -import { - // ... - createWorkflow, - transform, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +*** -// ... +## Access Long-Running Workflow Status and Result -export const syncBrandsFromCmsWorkflow = createWorkflow( - "sync-brands-from-system", - () => { - const brands = retrieveBrandsFromCmsStep() +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. - // TODO create and update brands - } -) -``` +To retrieve the workflow execution's details at a later point, you must enable [storing the workflow's executions](https://docs.medusajs.com/learn/fundamentals/workflows/store-executions/index.html.md). -In the workflow, you only use the `retrieveBrandsFromCmsStep` for now, which retrieves the brands from the third-party CMS. +For example: -Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use [transform](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values. +```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" -Learn more about data manipulation using `transform` in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). +export async function GET(req: MedusaRequest, res: MedusaResponse) { + const { transaction, result } = await myWorkflow(req.scope).run() -So, replace the `TODO` with the following: + const workflowEngineService = req.scope.resolve< + IWorkflowEngineService + >( + Modules.WORKFLOW_ENGINE + ) -```ts title="src/workflows/sync-brands-from-cms.ts" -const { toCreate, toUpdate } = transform( - { - brands, - }, - (data) => { - const toCreate: CreateBrand[] = [] - const toUpdate: UpdateBrand[] = [] + const subscriptionOptions = { + workflowId: "hello-world", + transactionId: transaction.transactionId, + subscriberId: "hello-world-subscriber", + } - data.brands.forEach((brand) => { - if (brand.external_id) { - toUpdate.push({ - id: brand.external_id as string, - name: brand.name as string, - }) - } else { - toCreate.push({ - name: brand.name as string, + 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) } - }) - - return { toCreate, toUpdate } - } -) + }, + }) -// TODO create and update the brands + res.send(result) +} ``` -`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 above example, you execute the long-running workflow `hello-world` and resolve the Workflow Engine Module's main service from the Medusa container. -In `transform`'s function, you loop over the brands array to check which should be created or updated. This logic assumes that a brand in the CMS has an `external_id` property whose value is the brand's ID in Medusa. +### subscribe Method -You now have the list of brands to create and update. So, replace the new `TODO` with the following: +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: -```ts title="src/workflows/sync-brands-from-cms.ts" -const created = createBrandsStep({ brands: toCreate }) -const updated = updateBrandsStep({ brands: toUpdate }) - -return new WorkflowResponse({ - created, - updated, -}) -``` - -You first run the `createBrandsStep` to create the brands that don't exist in Medusa, then the `updateBrandsStep` to update the brands that exist in Medusa. You pass the arrays returned by `transform` as the inputs for the steps. +- workflowId: (\`string\`) The name of the workflow. +- transactionId: (\`string\`) The ID of the workflow exection's transaction. The transaction's details are returned in the response of the workflow execution. +- subscriberId: (\`string\`) The ID of the subscriber. +- subscriber: (\`(data: \{ eventType: string, result?: any }) => Promise\\`) 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. -Finally, you return an object of the created and updated brands. You'll execute this workflow in the scheduled job next. +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 -## 2. Schedule Syncing Task +You can unsubscribe from the workflow using the workflow engine's `unsubscribe` method, which requires the same object parameter as the `subscribe` method. -You now have the workflow to sync the brands from the CMS to Medusa. Next, you'll create a scheduled job that runs this workflow once a day to ensure the data between Medusa and the CMS are always in sync. +However, instead of the `subscriber` property, it requires a `subscriberOrId` property whose value is the same `subscriberId` passed to the `subscribe` method. -A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. So, create the file `src/jobs/sync-brands-from-cms.ts` with the following content: +*** -![Directory structure of the Medusa application after adding the scheduled job](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494592/Medusa%20Book/cms-dir-overview-7_dkjb9s.jpg) +## Example: Restaurant-Delivery Recipe -```ts title="src/jobs/sync-brands-from-cms.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { syncBrandsFromCmsWorkflow } from "../workflows/sync-brands-from-cms" +To find a full example of a long-running workflow, refer to the [restaurant-delivery recipe](https://docs.medusajs.com/resources/recipes/marketplace/examples/restaurant-delivery/index.html.md). -export default async function (container: MedusaContainer) { - const logger = container.resolve("logger") +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. - const { result } = await syncBrandsFromCmsWorkflow(container).run() - logger.info( - `Synced brands from third-party system: ${ - result.created.length - } brands created and ${result.updated.length} brands updated.`) -} +# Multiple Step Usage in Workflow -export const config = { - name: "sync-brands-from-system", - schedule: "0 0 * * *", // change to * * * * * for debugging -} -``` +In this chapter, you'll learn how to use a step multiple times in a workflow. -A scheduled job file must export: +## Problem Reusing a Step in a Workflow -- An asynchronous function that will be executed at the specified schedule. This function must be the file's default export. -- An object of scheduled jobs configuration. It has two properties: - - `name`: A unique name for the scheduled job. - - `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. +In some cases, you may need to use a step multiple times in the same workflow. -The scheduled job function accepts as a parameter the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) used to resolve framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated. +The most common example is using the `useQueryGraphStep` multiple times in a workflow to retrieve multiple unrelated data, such as customers and products. -Based on the cron expression specified in `config.schedule`, Medusa will run the scheduled job every day at midnight. You can also change it to `* * * * *` to run it every minute for easier debugging. +Each workflow step must have a unique ID, which is the ID passed as a first parameter when creating the step: -*** +```ts +const useQueryGraphStep = createStep( + "use-query-graph" + // ... +) +``` -## Test it Out +This causes an error when you use the same step multiple times in a workflow, as it's registered in the workflow as two steps having the same ID: -To test out the scheduled job, start the Medusa application: +```ts +const helloWorkflow = createWorkflow( + "hello", + () => { + const { data: products } = useQueryGraphStep({ + entity: "product", + fields: ["id"], + }) -```bash npm2yarn -npm run dev + // ERROR OCCURS HERE: A STEP HAS THE SAME ID AS ANOTHER IN THE WORKFLOW + const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: ["id"], + }) + } +) ``` -If you set the schedule to `* * * * *` for debugging, the scheduled job will run in a minute. You'll see in the logs how many brands were created or updated. +The next section explains how to fix this issue to use the same step multiple times in a workflow. *** -## Summary - -By following the previous chapters, you utilized Medusa's framework and orchestration tools to perform and automate tasks that span across systems. +## How to Use a Step Multiple Times in a Workflow? -With Medusa, you can integrate any service from your commerce ecosystem with ease. You don't have to set up separate applications to manage your different customizations, or worry about data inconsistency across systems. Your efforts only go into implementing the business logic that ties your systems together. +When you execute a step in a workflow, you can chain a `config` method to it to change the step's config. +Use the `config` method to change a step's ID for a single execution. -# Guide: Define Module Link Between Brand and Product +So, this is the correct way to write the example above: -In this chapter, you'll learn how to define a module link between a brand defined in the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), and a product defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) that's available in your Medusa application out-of-the-box. +```ts highlights={highlights} +const helloWorkflow = createWorkflow( + "hello", + () => { + const { data: products } = useQueryGraphStep({ + entity: "product", + fields: ["id"], + }) -Modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) from other resources, ensuring that they're integrated into the Medusa application without side effects. However, you may need to associate data models of different modules, or you're trying to extend data models from commerce modules with custom properties. To do that, you define module links. + // ✓ No error occurs, the step has a different ID. + const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: ["id"], + }).config({ name: "fetch-customers" }) + } +) +``` -A module link forms an association between two data models of different modules while maintaining module isolation. You can then manage and query linked records of the data models using Medusa's Modules SDK. +The `config` method accepts an object with a `name` property. Its value is a new ID of the step to use for this execution only. -In this chapter, you'll define a module link between the `Brand` data model of the Brand Module, and the `Product` data model of the Product Module. In later chapters, you'll manage and retrieve linked product and brand records. +The first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`. -Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). -### Prerequisites +# Retry Failed Steps -- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +In this chapter, you’ll learn how to configure steps to allow retrial on failure. -## 1. Define Link +## Configure a Step’s Retrial -Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines and exports the link using `defineLink` from the Modules SDK. +By default, when an error occurs in a step, the step and the workflow fail, and the execution stops. -So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content: +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. -![The directory structure of the Medusa application after adding the link.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733329897/Medusa%20Book/brands-link-dir-overview_t1rhlp.jpg) +For example: -```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" +```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -export default defineLink( +const step1 = createStep( { - linkable: ProductModule.linkable.product, - isList: true, + name: "step-1", + maxRetries: 2, }, - BrandModule.linkable.brand -) -``` - -You import each module's definition object from the `index.ts` file of the module's directory. Each module object has a special `linkable` property that holds the data models' link configurations. + async () => { + console.log("Executing step 1") -The `defineLink` function accepts two parameters of the same type, which is either: + throw new Error("Oops! Something happened.") + } +) -- The data model's link configuration, which you access from the Module's `linkable` property; -- Or an object that has two properties: - - `linkable`: the data model's link configuration, which you access from the Module's `linkable` property. - - `isList`: A boolean indicating whether many records of the data model can be linked to the other model. +const myWorkflow = createWorkflow( + "hello-world", + function () { + const str1 = step1() -So, in the above code snippet, you define a link between the `Product` and `Brand` data models. Since a brand can be associated with multiple products, you enable `isList` in the `Product` model's object. + return new WorkflowResponse({ + message: str1, + }) +}) -*** +export default myWorkflow +``` -## 2. Sync the Link to the Database +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. -A module link is represented in the database as a table that stores the IDs of linked records. So, after defining the link, run the following command to create the module link's table in the database: +When you execute the above workflow, you’ll see the following result in the terminal: ```bash -npx medusa db:migrate +Executing step 1 +Executing step 1 +Executing step 1 +error: Oops! Something happened. +Error: Oops! Something happened. ``` -This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link. - -You can also run the `npx medusa db:sync-links` to just sync module links without running migrations. +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. *** -## Next Steps: Extend Create Product Flow - -In the next chapter, you'll extend Medusa's workflow and API route that create a product to allow associating a brand with a product. You'll also learn how to link brand and product records. +## 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. -# Guide: Integrate CMS Brand System +For example: -In the previous chapters, you've created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that adds brands to your application. In this chapter, you'll integrate a dummy Content-Management System (CMS) in a new module. The module's service will provide methods to retrieve and manage brands in the CMS. You'll later use this service to sync data from and to the CMS. +```ts title="src/workflows/hello-world.ts" highlights={[["5"]]} +const step1 = createStep( + { + name: "step-1", + maxRetries: 2, + retryInterval: 2, // 2 seconds + }, + async () => { + // ... + } +) +``` -Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +### Interval Changes Workflow to Long-Running -## 1. Create Module Directory +By setting `retryInterval` on a step, a workflow becomes a [long-running workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) that runs asynchronously in the background. So, you won't receive its result or errors immediately when you execute the workflow. -You'll integrate the third-party system in a new CMS Module. So, create the directory `src/modules/cms` that will hold the module's resources. +Instead, you must subscribe to the workflow's execution using the Workflow Engine Module Service. Learn more about it in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md). -![Directory structure after adding the directory for the CMS Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492447/Medusa%20Book/cms-dir-overview-1_gasguk.jpg) -*** +# Run Workflow Steps in Parallel -## 2. Create Module Service +In this chapter, you’ll learn how to run workflow steps in parallel. -Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system. +## parallelize Utility Function -Create the CMS Module's service at `src/modules/cms/service.ts` with the following content: +If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK. -![Directory structure after adding the CMS Module's service](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492583/Medusa%20Book/cms-dir-overview-2_zwcwh3.jpg) +The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step. -```ts title="src/modules/cms/service.ts" highlights={serviceHighlights} -import { Logger, ConfigModule } from "@medusajs/framework/types" +For example: -export type ModuleOptions = { - apiKey: string -} +```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" -type InjectedDependencies = { - logger: Logger - configModule: ConfigModule +interface WorkflowInput { + title: string } -class CmsModuleService { - private options_: ModuleOptions - private logger_: Logger +const myWorkflow = createWorkflow( + "my-workflow", + (input: WorkflowInput) => { + const product = createProductStep(input) - constructor({ logger }: InjectedDependencies, options: ModuleOptions) { - this.logger_ = logger - this.options_ = options + const [prices, productSalesChannel] = parallelize( + createPricesStep(product), + attachProductToSalesChannelStep(product) + ) - // TODO initialize SDK - } -} + const id = product.id + const refetchedProduct = getProductStep(product.id) -export default CmsModuleService + return new WorkflowResponse(refetchedProduct) + } +) ``` -You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters: - -1. The module's container. Since a module is [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), it has a [local container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) different than the Medusa container you use in other customizations. This container holds framework tools like the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) and resources within the module. -2. Options passed to the module when it's later added in Medusa's configurations. These options are useful to pass secret keys or configurations that ensure your module is re-usable across applications. For the CMS Module, you accept the API key to connect to the dummy CMS as an option. - -When integrating a third-party system that has a Node.js SDK or client, you can initialize that client in the constructor to be used in the service's methods. - -### Integration Methods +The `parallelize` function accepts the steps to run in parallel as a parameter. -Next, you'll add methods that simulate sending requests to a third-party CMS. You'll use these methods later to sync brands from and to the CMS. +It returns an array of the steps' results in the same order they're passed to the `parallelize` function. -Add the following methods in the `CmsModuleService`: +So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`. -```ts title="src/modules/cms/service.ts" highlights={methodsHighlights} -export class CmsModuleService { - // ... - // 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}.`) - this.logger_.info(`Request Data: ${JSON.stringify(data, null, 2)}`) - this.logger_.info(`API Key: ${JSON.stringify(this.options_.apiKey, null, 2)}`) - } +# Store Workflow Executions - async createBrand(brand: Record) { - await this.sendRequest("/brands", "POST", brand) - } +In this chapter, you'll learn how to store workflow executions in the database and access them later. - async deleteBrand(id: string) { - await this.sendRequest(`/brands/${id}`, "DELETE") - } +## Workflow Execution Retention - async retrieveBrands(): Promise[]> { - await this.sendRequest("/brands", "GET") +Medusa doesn't store your workflow's execution details by default. However, you can configure a workflow to keep its execution details stored in the database. - return [] - } -} -``` +This is useful for auditing and debugging purposes. When you store a workflow's execution, you can view details around its steps, their states and their output. You can also check whether the workflow or any of its steps failed. -The `sendRequest` method sends requests to the third-party CMS. Since this guide isn't using a real CMS, it only simulates the sending by logging messages in the terminal. +You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows. -You also add three methods that use the `sendRequest` method: +*** -- `createBrand` that creates a brand in the third-party system. -- `deleteBrand` that deletes the brand in the third-party system. -- `retrieveBrands` to retrieve a brand from the third-party system. +## How to Store Workflow's Executions? -*** +### Prerequisites -## 3. Export Module Definition +- [Redis Workflow Engine must be installed and configured.](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/redis/index.html.md) -After creating the module's service, you'll export the module definition indicating the module's name and service. +`createWorkflow` from the Workflows SDK can accept an object as a first parameter to set the workflow's configuration. To enable storing a workflow's executions: -Create the file `src/modules/cms/index.ts` with the following content: +- Enable the `store` option. If your workflow is a [Long-Running Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md), this option is enabled by default. +- Set the `retentionTime` option to the number of seconds that the workflow execution should be stored in the database. -![Directory structure of the Medusa application after adding the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492991/Medusa%20Book/cms-dir-overview-3_b0byks.jpg) +For example: -```ts title="src/modules/cms/index.ts" -import { Module } from "@medusajs/framework/utils" -import CmsModuleService from "./service" +```ts highlights={highlights} +import { createStep, createWorkflow } from "@medusajs/framework/workflows-sdk" -export const CMS_MODULE = "cms" +const step1 = createStep( + { + name: "step-1", + }, + async () => { + console.log("Hello from step 1") + } +) -export default Module(CMS_MODULE, { - service: CmsModuleService, -}) +export const helloWorkflow = createWorkflow( + { + name: "hello-workflow", + retentionTime: 99999, + store: true, + }, + () => { + step1() + } +) ``` -You use `Module` from the Modules SDK to export the module's defintion, indicating that the module's name is `cms` and its service is `CmsModuleService`. +Whenever you execute the `helloWorkflow` now, its execution details will be stored in the database. *** -## 4. Add Module to Medusa's Configurations +## Retrieve Workflow Executions -Finally, add the module to the Medusa configurations at `medusa-config.ts`: +You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows. -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - // ... - { - resolve: "./src/modules/cms", - options: { - apiKey: process.env.CMS_API_KEY, - }, - }, - ], -}) +When you execute a workflow, the returned object has a `transaction` property containing the workflow execution's transaction details: + +```ts +const { transaction } = await helloWorkflow(container).run() ``` -The object passed in `modules` accept an `options` property, whose value is an object of options to pass to the module. These are the options you receive in the `CmsModuleService`'s constructor. +To retrieve a workflow's execution details from the database, resolve the Workflow Engine Module from the container and use its `listWorkflowExecutions` method. -You can add the `CMS_API_KEY` environment variable to `.env`: +For example, you can create a `GET` API Route at `src/workflows/[id]/route.ts` that retrieves a workflow execution for the specified transaction ID: -```bash -CMS_API_KEY=123 -``` +```ts title="src/workflows/[id]/route.ts" highlights={retrieveHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework" +import { Modules } from "@medusajs/framework/utils" -*** +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { transaction_id } = req.params + + const workflowEngineService = req.scope.resolve( + Modules.WORKFLOW_ENGINE + ) -## Next Steps: Sync Brand From Medusa to CMS + const [workflowExecution] = await workflowEngineService.listWorkflowExecutions({ + transaction_id: transaction_id, + }) -You can now use the CMS Module's service to perform actions on the third-party CMS. + res.json({ + workflowExecution, + }) +} +``` -In the next chapter, you'll learn how to emit an event when a brand is created, then handle that event to sync the brand from Medusa to the third-party service. +In the above example, you resolve the Workflow Engine Module from the container and use its `listWorkflowExecutions` method, passing the `transaction_id` as a filter to retrieve its workflow execution details. +A workflow execution object will be similar to the following: -# Guide: Extend Create Product Flow +```json +{ + "workflow_id": "hello-workflow", + "transaction_id": "01JJC2T6AVJCQ3N4BRD1EB88SP", + "id": "wf_exec_01JJC2T6B3P76JD35F12QTTA78", + "execution": { + "state": "done", + "steps": {}, + "modelId": "hello-workflow", + "options": {}, + "metadata": {}, + "startedAt": 1737719880027, + "definition": {}, + "timedOutAt": null, + "hasAsyncSteps": false, + "transactionId": "01JJC2T6AVJCQ3N4BRD1EB88SP", + "hasFailedSteps": false, + "hasSkippedSteps": false, + "hasWaitingSteps": false, + "hasRevertedSteps": false, + "hasSkippedOnFailureSteps": false + }, + "context": { + "data": {}, + "errors": [] + }, + "state": "done", + "created_at": "2025-01-24T09:58:00.036Z", + "updated_at": "2025-01-24T09:58:00.046Z", + "deleted_at": null +} +``` -After linking the [custom Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) in the [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md), you'll extend the create product workflow and API route to allow associating a brand with a product. +### Example: Check if Stored Workflow Execution Failed -Some API routes, including the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), accept an `additional_data` request body parameter. This parameter can hold custom data that's passed to the [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) of the workflow executed in the API route, allowing you to consume those hooks and perform actions with the custom data. +To check if a stored workflow execution failed, you can check its `state` property: -So, in this chapter, to extend the create product flow and associate a brand with a product, you will: +```ts +if (workflowExecution.state === "failed") { + return res.status(500).json({ + error: "Workflow failed", + }) +} +``` -- Consume the [productsCreated](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow#productsCreated/index.html.md) hook of the [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md), which is executed within the workflow after the product is created. You'll link the product with the brand passed in the `additional_data` parameter. -- Extend the Create Product API route to allow passing a brand ID in `additional_data`. +Other state values include `done`, `invoking`, and `compensating`. -To learn more about the `additional_data` property and the API routes that accept additional data, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md). -### Prerequisites +# Variable Manipulation in Workflows with transform -- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) -- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) +In this chapter, you'll learn how to use `transform` from the Workflows SDK to manipulate variables in a workflow. -*** +## Why Variable Manipulation isn't Allowed in Workflows -## 1. Consume the productsCreated Hook +Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. -A workflow hook is a point in a workflow where you can inject a step to perform a custom functionality. Consuming a workflow hook allows you to extend the features of a workflow and, consequently, the API route that uses it. +At that point, variables in the workflow don't have any values. They only do when you execute the workflow. -Learn more about the workflow hooks in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). +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. -The [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) used in the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts) has a `productsCreated` hook that runs after the product is created. You'll consume this hook to link the created product with the brand specified in the request parameters. +Instead, use `transform` from the Workflows SDK. -To consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content: +Restrictions for variable manipulation is only applicable in a workflow's definition. You can still manipulate variables in your step's code. -![Directory structure after creating the hook's file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733384338/Medusa%20Book/brands-hook-dir-overview_ltwr5h.jpg) +*** -```ts title="src/workflows/hooks/created-product.ts" highlights={hook1Highlights} -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" -import { StepResponse } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" -import { LinkDefinition } from "@medusajs/framework/types" -import { BRAND_MODULE } from "../../modules/brand" -import BrandModuleService from "../../modules/brand/service" +## What is the transform Utility? -createProductsWorkflow.hooks.productsCreated( - (async ({ products, additional_data }, { container }) => { - if (!additional_data?.brand_id) { - return new StepResponse([], []) - } +`transform` creates a new variable as the result of manipulating other variables. - 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) +For example, consider you have two strings as the output of two steps: - // TODO link brand to product - }) -) +```ts +const str1 = step1() +const str2 = step2() ``` -Workflows have a special `hooks` property to access its hooks and consume them. Each hook, such as `productsCreated`, accepts a step function as a parameter. The step function accepts the following parameters: - -1. An object having an `additional_data` property, which is the custom data passed in the request body under `additional_data`. The object will also have properties passed from the workflow to the hook, which in this case is the `products` property that holds an array of the created products. -2. An object of properties related to the step's context. It has a `container` property whose value is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) to resolve framework and commerce tools. +To concatenate the strings, you create a new variable `str3` using the `transform` function: -In the step, if a brand ID is passed in `additional_data`, you resolve the Brand Module's service and use its generated `retrieveBrand` method to retrieve the brand by its ID. The `retrieveBrand` method will throw an error if the brand doesn't exist. +```ts highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + transform, +} from "@medusajs/framework/workflows-sdk" +// step imports... -### Link Brand to Product +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str1 = step1(input) + const str2 = step2(input) -Next, you want to create a link between the created products and the brand. To do so, you use Link, which is a class from the Modules SDK that provides methods to manage linked records. + const str3 = transform( + { str1, str2 }, + (data) => `${data.str1}${data.str2}` + ) -Learn more about Link in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md). + return new WorkflowResponse(str3) + } +) +``` -To use Link in the `productsCreated` hook, replace the `TODO` with the following: +`transform` accepts two parameters: -```ts title="src/workflows/hooks/created-product.ts" highlights={hook2Highlights} -const link = container.resolve("link") -const logger = container.resolve("logger") +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. -const links: LinkDefinition[] = [] +The value returned by the second parameter function is returned by `transform`. So, the `str3` variable holds the concatenated string. -for (const product of products) { - links.push({ - [Modules.PRODUCT]: { - product_id: product.id, - }, - [BRAND_MODULE]: { - brand_id: additional_data.brand_id, - }, - }) -} +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. -await link.create(links) +*** -logger.info("Linked brand to products") +## Example: Looping Over Array -return new StepResponse(links, links) +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) + + // ... + } +) ``` -You resolve Link from the container. Then you loop over the created products to assemble an array of links to be created. After that, you pass the array of links to Link's `create` method, which will link the product and brand records. +This workflow receives an `items` array in its input. -Each property in the link object is the name of a module, and its value is an object having a `{model_name}_id` property, where `{model_name}` is the snake-case name of the module's data model. Its value is the ID of the record to be linked. The link object's properties must be set in the same order as the link configurations passed to `defineLink`. +You use `transform` to create an `ids` variable, which is an array of strings holding the `id` of each item in the `items` array. -![Diagram showcasing how the order of defining a link affects creating the link](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386156/Medusa%20Book/remote-link-brand-product-exp_fhjmg4.jpg) +You then pass the `ids` variable as a parameter to the `doSomethingStep`. -Finally, you return an instance of `StepResponse` returning the created links. +*** -### Dismiss Links in Compensation +## Example: Creating a Date -You can pass as a second parameter of the hook a compensation function that undoes what the step did. It receives as a first parameter the returned `StepResponse`'s second parameter, and the step context object as a second parameter. +If you create a date with `new Date()` in a workflow's constructor function, Medusa evaluates the date's value when it creates the internal representation of the workflow, not when the workflow is executed. -To undo creating the links in the hook, pass the following compensation function as a second parameter to `productsCreated`: +So, use `transform` instead to create a date variable with `new Date()`. -```ts title="src/workflows/hooks/created-product.ts" -createProductsWorkflow.hooks.productsCreated( - // ... - (async (links, { container }) => { - if (!links?.length) { - return - } +For example: - const link = container.resolve("link") +```ts +const myWorkflow = createWorkflow( + "hello-world", + () => { + const today = transform({}, () => new Date()) - await link.dismiss(links) - }) + doSomethingStep(today) + } ) ``` -In the compensation function, if the `links` parameter isn't empty, you resolve Link from the container and use its `dismiss` method. This method removes a link between two records. It accepts the same parameter as the `create` method. +In this workflow, `today` is only evaluated when the workflow is executed. *** -## 2. Configure Additional Data Validation - -Now that you've consumed the `productsCreated` hook, you want to configure the `/admin/products` API route that creates a new product to accept a brand ID in its `additional_data` parameter. +## Caveats -You configure the properties accepted in `additional_data` in the `src/api/middlewares.ts` that exports middleware configurations. So, create the file (or, if already existing, add to the file) `src/api/middlewares.ts` the following content: +### Transform Evaluation -![Directory structure after adding the middelwares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386868/Medusa%20Book/brands-middleware-dir-overview_uczos1.jpg) +`transform`'s value is only evaluated if you pass its output to a step or in the workflow response. -```ts title="src/api/middlewares.ts" -import { defineMiddlewares } from "@medusajs/framework/http" -import { z } from "zod" +For example, if you have the following workflow: -// ... +```ts +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str = transform( + { input }, + (data) => `${data.input.str1}${data.input.str2}` + ) -export default defineMiddlewares({ - routes: [ - // ... - { - matcher: "/admin/products", - method: ["POST"], - additionalDataValidator: { - brand_id: z.string().optional(), - }, - }, - ], -}) + return new WorkflowResponse("done") + } +) ``` -Objects in `routes` accept an `additionalDataValidator` property that configures the validation rules for custom properties passed in the `additional_data` request parameter. It accepts an object whose keys are custom property names, and their values are validation rules created using [Zod](https://zod.dev/). +Since `str`'s value isn't used as a step's input or passed to `WorkflowResponse`, its value is never evaluated. -So, `POST` requests sent to `/admin/products` can now pass the ID of a brand in the `brand_id` property of `additional_data`. +### Data Validation -*** +`transform` should only be used to perform variable or data manipulation. -## Test it Out +If you want to perform some validation on the data, use a step or [when-then](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md) instead. -To test it out, first, retrieve the authentication token of your admin user by sending a `POST` request to `/auth/user/emailpass`: +For example: -```bash -curl -X POST 'http://localhost:9000/auth/user/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "admin@medusa-test.com", - "password": "supersecret" -}' -``` +```ts +// DON'T +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str = transform( + { input }, + (data) => { + if (!input.str1) { + throw new Error("Not allowed!") + } + } + ) + } +) -Make sure to replace the email and password in the request body with your user's credentials. +// DO +const validateHasStr1Step = createStep( + "validate-has-str1", + ({ input }) => { + if (!input.str1) { + throw new Error("Not allowed!") + } + } +) -Then, send a `POST` request to `/admin/products` to create a product, and pass in the `additional_data` parameter a brand's ID: +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + validateHasStr1({ + input, + }) -```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"] - } - ], - "shipping_profile_id": "{shipping_profile_id}", - "additional_data": { - "brand_id": "{brand_id}" - } -}' + // workflow continues its execution only if + // the step doesn't throw the error. + } +) ``` -Make sure to replace `{token}` with the token you received from the previous request, `shipping_profile_id` with the ID of a shipping profile in your application, and `{brand_id}` with the ID of a brand in your application. You can retrieve the ID of a shipping profile either from the Medusa Admin, or the [List Shipping Profiles API route](https://docs.medusajs.com/api/admin#shipping-profiles_getshippingprofiles). - -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. +# Workflow Hooks -*** +In this chapter, you'll learn what a workflow hook is and how to consume them. -## Next Steps: Query Linked Brands and Products +## What is a Workflow Hook? -Now that you've extending the create-product flow to link a brand to it, you want to retrieve the brand details of a product. You'll learn how to do so in the next chapter. +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. -# Guide: Query Product's Brands +Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks. -In the previous chapters, you [defined a link](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) between the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md), then [extended the create-product flow](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product/index.html.md) to link a product to a brand. +You want to perform a custom action during a workflow's execution, such as when a product is created. -In this chapter, you'll learn how to retrieve a product's brand (and vice-versa) in two ways: Using Medusa's existing API route, or in customizations, such as a custom API route. +*** -### Prerequisites +## How to Consume a Hook? -- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) -- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) +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: -## Approach 1: Retrieve Brands in Existing API Routes +- Import the workflow. +- Access its hook using the `hooks` property. +- Pass the hook a step function as a parameter to consume it. -Medusa's existing API routes accept a `fields` query parameter that allows you to specify the fields and relations of a model to retrieve. So, when you send a request to the [List Products](https://docs.medusajs.com/api/admin#products_getproducts), [Get Product](https://docs.medusajs.com/api/admin#products_getproductsid), or any product-related store or admin routes that accept a `fields` query parameter, you can specify in this parameter to return the product's brands. +For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content: -Learn more about selecting fields and relations in the [API Reference](https://docs.medusajs.com/api/admin#select-fields-and-relations). +```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" -For example, send the following request to retrieve the list of products with their brands: - -```bash -curl 'http://localhost:9000/admin/products?fields=+brand.*' \ ---header 'Authorization: Bearer {token}' +createProductsWorkflow.hooks.productsCreated( + async ({ products }, { container }) => { + // TODO perform an action + } +) ``` -Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). +The `productsCreated` hook is available on the workflow's `hooks` property by its name. -Any product that is linked to a brand will have a `brand` property in its object: +You invoke the hook, passing a step function (the hook handler) as a parameter. -```json title="Example Product Object" -{ - "id": "prod_123", - // ... - "brand": { - "id": "01JEB44M61BRM3ARM2RRMK7GJF", - "name": "Acme", - "created_at": "2024-12-05T09:59:08.737Z", - "updated_at": "2024-12-05T09:59:08.737Z", - "deleted_at": null - } -} -``` +Now, when a product is created using the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), your hook handler is executed after the product is created. -By using the `fields` query parameter, you don't have to re-create existing API routes to get custom data models that you linked to core data models. +A hook can have only one handler. -*** +Refer to the [createProductsWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) to see at which point the hook handler is executed. -## Approach 2: Use Query to Retrieve Linked Records +### Hook Handler Parameter -You can also retrieve linked records using Query. Query allows you to retrieve data across modules with filters, pagination, and more. You can resolve Query from the Medusa container and use it in your API route or workflow. +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. -Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). +Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product. -For example, you can create an API route that retrieves brands and their products. If you followed the [Create Brands API route chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll have the file `src/api/admin/brands/route.ts` with a `POST` API route. Add a new `GET` function to the same file: +### Hook Handler Compensation -```ts title="src/api/admin/brands/route.ts" highlights={highlights} -// other imports... -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook. -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve("query") - - const { data: brands } = await query.graph({ - entity: "brand", - fields: ["*", "products.*"], - }) +For example: - res.json({ brands }) -} -``` +```ts title="src/workflows/hooks/product-created.ts" +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" -This adds a `GET` API route at `/admin/brands`. In the API route, you resolve Query from the Medusa container. Query has a `graph` method that runs a query to retrieve data. It accepts an object having the following properties: +createProductsWorkflow.hooks.productsCreated( + async ({ products }, { container }) => { + // TODO perform an action -- `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`, or `*` for all properties. - - A relation or linked model's name, such as `products` (use the plural name since brands are linked to list of products). You suffix the name with `.*` to retrieve all its properties. + return new StepResponse(undefined, { ids }) + }, + async ({ ids }, { container }) => { + // undo the performed action + } +) +``` -`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. +The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler. -### Test it Out +The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function. -To test the API route out, send a `GET` request to `/admin/brands`: +It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container. -```bash -curl 'http://localhost:9000/admin/brands' \ --H 'Authorization: Bearer {token}' -``` +### Additional Data Property -Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). +Medusa's workflows pass in the hook's input an `additional_data` property: -This returns the brands in your store with their linked products. For example: +```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" -```json title="Example Response" -{ - "brands": [ - { - "id": "123", - // ... - "products": [ - { - "id": "prod_123", - // ... - } - ] - } - ] -} +createProductsWorkflow.hooks.productsCreated( + async ({ products, additional_data }, { container }) => { + // TODO perform an action + } +) ``` -*** - -## Summary +This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow. -By following the examples of the previous chapters, you: +Learn how to pass `additional_data` in requests to API routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md). -- Defined a link between the Brand and Product modules's data models, allowing you to associate a product with a brand. -- Extended the create-product workflow and route to allow setting the product's brand while creating the product. -- Queried a product's brand, and vice versa. +### 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: -## Next Steps: Customize Medusa Admin +```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" -Clients, such as the Medusa Admin dashboard, can now use brand-related features, such as creating a brand or setting the brand of a product. +export async function POST(req: MedusaRequest, res: MedusaResponse) { + await createProductsWorkflow(req.scope).run({ + input: { + products: [ + // ... + ], + additional_data: { + custom_field: "test", + }, + }, + }) +} +``` -In the next chapters, you'll learn how to customize the Medusa Admin to show a product's brand on its details page, and to show a new page with the list of brands in your store. +Your hook handler then receives that passed data in the `additional_data` object. -# Docs Contribution Guidelines +# Workflow Timeout -Thank you for your interest in contributing to the documentation! You will be helping the open source community and other developers interested in learning more about Medusa and using it. +In this chapter, you’ll learn how to set a timeout for workflows and steps. -This guide is specific to contributing to the documentation. If you’re interested in contributing to Medusa’s codebase, check out the [contributing guidelines in the Medusa GitHub repository](https://github.com/medusajs/medusa/blob/develop/CONTRIBUTING.md). +## What is a Workflow Timeout? -## Documentation Workspace +By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. -Medusa's documentation projects are all part of the documentation yarn workspace, which you can find in the [medusa repository](https://github.com/medusajs/medusa) under the `www` directory. +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. -The workspace has the following two directories: +### Timeout Doesn't Stop Step Execution -- `apps`: this directory holds the different documentation websites and projects. - - `book`: includes the codebase for the [main Medusa documentation](https://docs.medusajs.com//index.html.md). It's built with [Next.js 15](https://nextjs.org/). - - `resources`: includes the codebase for the resources documentation, which powers different sections of the docs such as the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) or [How-to & Tutorials](https://docs.medusajs.com/resources/how-to-tutorials/index.html.md) sections. It's built with [Next.js 15](https://nextjs.org/). - - `api-reference`: includes the codebase for the API reference website. It's built with [Next.js 15](https://nextjs.org/). - - `ui`: includes the codebase for the Medusa UI documentation website. It's built with [Next.js 15](https://nextjs.org/). -- `packages`: this directory holds the shared packages and components necessary for the development of the projects in the `apps` directory. - - `docs-ui` includes the shared React components between the different apps. - - `remark-rehype-plugins` includes Remark and Rehype plugins used by the documentation projects. +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. *** -## Documentation Content - -All documentation projects are built with Next.js. The content is writtin in MDX files. - -### Medusa Main Docs Content +## Configure Workflow Timeout -The content of the Medusa main docs are under the `www/apps/book/app` directory. +The `createWorkflow` function can accept a configuration object instead of the workflow’s name. -### Medusa Resources Content +In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. -The content of all pages under the `/resources` path are under the `www/apps/resources/app` directory. +For example: -Documentation pages under the `www/apps/resources/references` directory are generated automatically from the source code under the `packages/medusa` directory. So, you can't directly make changes to them. Instead, you'll have to make changes to the comments in the original source code. +```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" +import { + createStep, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -### API Reference +const step1 = createStep( + "step-1", + async () => { + // ... + } +) -The API reference's content is split into two types: +const myWorkflow = createWorkflow({ + name: "hello-world", + timeout: 2, // 2 seconds +}, function () { + const str1 = step1() -1. Static content, which are the content related to getting started, expanding fields, and more. These are located in the `www/apps/api-reference/markdown` directory. They are MDX files. -2. OpenAPI specs that are shown to developers when checking the reference of an API Route. These are generated from OpenApi Spec comments, which are under the `www/utils/generated/oas-output` directory. + return new WorkflowResponse({ + message: str1, + }) +}) -### Medusa UI Documentation +export default myWorkflow -The content of the Medusa UI documentation are located under the `www/apps/ui/src/content/docs` directory. They are MDX files. +``` -The UI documentation also shows code examples, which are under the `www/apps/ui/src/examples` directory. +This workflow's executions fail if they run longer than two seconds. -The UI component props are generated from the source code and placed into the `www/apps/ui/src/specs` directory. To contribute to these props and their comments, check the comments in the source code under the `packages/design-system/ui` directory. +A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/access-workflow-errors/index.html.md). The error’s name is `TransactionTimeoutError`. *** -## Style Guide +## Configure Step Timeout -When you contribute to the documentation content, make sure to follow the [documentation style guide](https://www.notion.so/Style-Guide-Docs-fad86dd1c5f84b48b145e959f36628e0). +Alternatively, you can configure the timeout for a step rather than the entire workflow. -*** +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. -## How to Contribute +The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. -If you’re fixing errors in an existing documentation page, you can scroll down to the end of the page and click on the “Edit this page” link. You’ll be redirected to the GitHub edit form of that page and you can make edits directly and submit a pull request (PR). +For example: -If you’re adding a new page or contributing to the codebase, fork the repository, create a new branch, and make all changes necessary in your repository. Then, once you’re done, create a PR in the Medusa repository. +```tsx +const step1 = createStep( + { + name: "step-1", + timeout: 2, // 2 seconds + }, + async () => { + // ... + } +) +``` -### Base Branch +This step's executions fail if they run longer than two seconds. -When you make an edit to an existing documentation page or fork the repository to make changes to the documentation, create a new branch. +A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/access-workflow-errors/index.html.md). The error’s name is `TransactionStepTimeoutError`. -Documentation contributions always use `develop` as the base branch. Make sure to also open your PR against the `develop` branch. -### Branch Name +# Translate Medusa Admin -Make sure that the branch name starts with `docs/`. For example, `docs/fix-services`. Vercel deployed previews are only triggered for branches starting with `docs/`. +The Medusa Admin supports multiple languages, with the default being English. In this documentation, you'll learn how to contribute to the community by translating the Medusa Admin to a language you're fluent in. -### Pull Request Conventions +{/* vale docs.We = NO */} -When you create a pull request, prefix the title with `docs:` or `docs(PROJECT_NAME):`, where `PROJECT_NAME` is the name of the documentation project this pull request pertains to. For example, `docs(ui): fix titles`. +You can contribute either by translating the admin to a new language, or fixing translations for existing languages. As we can't validate every language's translations, some translations may be incorrect. Your contribution is welcome to fix any translation errors you find. -In the body of the PR, explain clearly what the PR does. If the PR solves an issue, use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) with the issue number. For example, “Closes #1333”. +{/* vale docs.We = YES */} + +Check out the translated languages either in the admin dashboard's settings or on [GitHub](https://github.com/medusajs/medusa/blob/develop/packages/admin/dashboard/src/i18n/languages.ts). *** -## Images +## How to Contribute Translation -If you are adding images to a documentation page, you can host the image on [Imgur](https://imgur.com) for free to include it in the PR. Our team will later upload it to our image hosting. +1. Clone the [Medusa monorepository](https://github.com/medusajs/medusa) to your local machine: -*** +```bash +git clone https://github.com/medusajs/medusa.git +``` + +If you already have it cloned, make sure to pull the latest changes from the `develop` branch. + +2. Install the monorepository's dependencies. Since it's a Yarn workspace, it's highly recommended to use yarn: + +```bash +yarn install +``` + +3. Create a branch that you'll use to open the pull request later: + +```bash +git checkout -b feat/translate- +``` + +Where `` is your language name. For example, `feat/translate-da`. + +4. Translation files are under `packages/admin/dashboard/src/i18n/translations` as JSON files whose names are the ISO-2 name of the language. + - If you're adding a new language, copy the file `packages/admin/dashboard/src/i18n/translations/en.json` and paste it with the ISO-2 name for your language. For example, if you're adding Danish translations, copy the `en.json` file and paste it as `packages/admin/dashboard/src/i18n/translations/de.json`. + - If you're fixing a translation, find the JSON file of the language under `packages/admin/dashboard/src/i18n/translations`. + +5. Start translating the keys in the JSON file (or updating the targeted ones). All keys in the JSON file must be translated, and your PR tests will fail otherwise. + - You can check whether the JSON file is valid by running the following command in `packages/admin/dashboard`, replacing `da.json` with the JSON file's name: + +```bash title="packages/admin/dashboard" +yarn i18n:validate da.json +``` + +6. After finishing the translation, if you're adding a new language, import its JSON file in `packages/admin/dashboard/src/i18n/translations/index.ts` and add it to the exported object: + +```ts title="packages/admin/dashboard/src/i18n/translations/index.ts" highlights={[["2"], ["6"], ["7"], ["8"]]} +// other imports... +import da from "./da.json" + +export default { + // other languages... + da: { + translation: da, + }, +} +``` + +The language's key in the object is the ISO-2 name of the language. + +7. If you're adding a new language, add it to the file `packages/admin/dashboard/src/i18n/languages.ts`: + +```ts title="packages/admin/dashboard/src/i18n/languages.ts" highlights={languageHighlights} +import { da } from "date-fns/locale" +// other imports... + +export const languages: Language[] = [ + // other languages... + { + code: "da", + display_name: "Danish", + ltr: true, + date_locale: da, + }, +] +``` + +`languages` is an array having the following properties: + +- `code`: The ISO-2 name of the language. For example, `da` for Danish. +- `display_name`: The language's name to be displayed in the admin. +- `ltr`: Whether the language supports a left-to-right layout. For example, set this to `false` for languages like Arabic. +- `date_locale`: An instance of the locale imported from the [date-fns/locale](https://date-fns.org/) package. + +8. Once you're done, push the changes into your branch and open a pull request on GitHub. + +Our team will perform a general review on your PR and merge it if no issues are found. The translation will be available in the admin after the next release. + + +# Docs Contribution Guidelines + +Thank you for your interest in contributing to the documentation! You will be helping the open source community and other developers interested in learning more about Medusa and using it. + +This guide is specific to contributing to the documentation. If you’re interested in contributing to Medusa’s codebase, check out the [contributing guidelines in the Medusa GitHub repository](https://github.com/medusajs/medusa/blob/develop/CONTRIBUTING.md). + +## Documentation Workspace + +Medusa's documentation projects are all part of the documentation yarn workspace, which you can find in the [medusa repository](https://github.com/medusajs/medusa) under the `www` directory. + +The workspace has the following two directories: + +- `apps`: this directory holds the different documentation websites and projects. + - `book`: includes the codebase for the [main Medusa documentation](https://docs.medusajs.com//index.html.md). It's built with [Next.js 15](https://nextjs.org/). + - `resources`: includes the codebase for the resources documentation, which powers different sections of the docs such as the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) or [How-to & Tutorials](https://docs.medusajs.com/resources/how-to-tutorials/index.html.md) sections. It's built with [Next.js 15](https://nextjs.org/). + - `api-reference`: includes the codebase for the API reference website. It's built with [Next.js 15](https://nextjs.org/). + - `ui`: includes the codebase for the Medusa UI documentation website. It's built with [Next.js 15](https://nextjs.org/). +- `packages`: this directory holds the shared packages and components necessary for the development of the projects in the `apps` directory. + - `docs-ui` includes the shared React components between the different apps. + - `remark-rehype-plugins` includes Remark and Rehype plugins used by the documentation projects. + +*** + +## Documentation Content + +All documentation projects are built with Next.js. The content is writtin in MDX files. + +### Medusa Main Docs Content + +The content of the Medusa main docs are under the `www/apps/book/app` directory. + +### Medusa Resources Content + +The content of all pages under the `/resources` path are under the `www/apps/resources/app` directory. + +Documentation pages under the `www/apps/resources/references` directory are generated automatically from the source code under the `packages/medusa` directory. So, you can't directly make changes to them. Instead, you'll have to make changes to the comments in the original source code. + +### API Reference + +The API reference's content is split into two types: + +1. Static content, which are the content related to getting started, expanding fields, and more. These are located in the `www/apps/api-reference/markdown` directory. They are MDX files. +2. OpenAPI specs that are shown to developers when checking the reference of an API Route. These are generated from OpenApi Spec comments, which are under the `www/utils/generated/oas-output` directory. + +### Medusa UI Documentation + +The content of the Medusa UI documentation are located under the `www/apps/ui/src/content/docs` directory. They are MDX files. + +The UI documentation also shows code examples, which are under the `www/apps/ui/src/examples` directory. + +The UI component props are generated from the source code and placed into the `www/apps/ui/src/specs` directory. To contribute to these props and their comments, check the comments in the source code under the `packages/design-system/ui` directory. + +*** + +## Style Guide + +When you contribute to the documentation content, make sure to follow the [documentation style guide](https://www.notion.so/Style-Guide-Docs-fad86dd1c5f84b48b145e959f36628e0). + +*** + +## How to Contribute + +If you’re fixing errors in an existing documentation page, you can scroll down to the end of the page and click on the “Edit this page” link. You’ll be redirected to the GitHub edit form of that page and you can make edits directly and submit a pull request (PR). + +If you’re adding a new page or contributing to the codebase, fork the repository, create a new branch, and make all changes necessary in your repository. Then, once you’re done, create a PR in the Medusa repository. + +### Base Branch + +When you make an edit to an existing documentation page or fork the repository to make changes to the documentation, create a new branch. + +Documentation contributions always use `develop` as the base branch. Make sure to also open your PR against the `develop` branch. + +### Branch Name + +Make sure that the branch name starts with `docs/`. For example, `docs/fix-services`. Vercel deployed previews are only triggered for branches starting with `docs/`. + +### Pull Request Conventions + +When you create a pull request, prefix the title with `docs:` or `docs(PROJECT_NAME):`, where `PROJECT_NAME` is the name of the documentation project this pull request pertains to. For example, `docs(ui): fix titles`. + +In the body of the PR, explain clearly what the PR does. If the PR solves an issue, use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) with the issue number. For example, “Closes #1333”. + +*** + +## Images + +If you are adding images to a documentation page, you can host the image on [Imgur](https://imgur.com) for free to include it in the PR. Our team will later upload it to our image hosting. + +*** ## NPM and Yarn Code Blocks @@ -15377,128 +15270,101 @@ console.log("This block can't use semi colons") ~~~ */} -# Translate Medusa Admin +# Write Integration Tests -The Medusa Admin supports multiple languages, with the default being English. In this documentation, you'll learn how to contribute to the community by translating the Medusa Admin to a language you're fluent in. +In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests. -{/* vale docs.We = NO */} +### Prerequisites -You can contribute either by translating the admin to a new language, or fixing translations for existing languages. As we can't validate every language's translations, some translations may be incorrect. Your contribution is welcome to fix any translation errors you find. +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -{/* vale docs.We = YES */} +## medusaIntegrationTestRunner Utility -Check out the translated languages either in the admin dashboard's settings or on [GitHub](https://github.com/medusajs/medusa/blob/develop/packages/admin/dashboard/src/i18n/languages.ts). +The `medusaIntegrationTestRunner` is from Medusa's Testing Framework and it's used 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: -## How to Contribute Translation +```ts title="integration-tests/http/test.spec.ts" highlights={highlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -1. Clone the [Medusa monorepository](https://github.com/medusajs/medusa) to your local machine: +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + // TODO write tests... + }, +}) -```bash -git clone https://github.com/medusajs/medusa.git +jest.setTimeout(60 * 1000) ``` -If you already have it cloned, make sure to pull the latest changes from the `develop` branch. +The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`. -2. Install the monorepository's dependencies. Since it's a Yarn workspace, it's highly recommended to use yarn: +`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: -```bash -yarn install -``` +- `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. -3. Create a branch that you'll use to open the pull request later: +The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). -```bash -git checkout -b feat/translate- +### Jest Timeout + +Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test: + +```ts title="integration-tests/http/test.spec.ts" +// in your test's file +jest.setTimeout(60 * 1000) ``` -Where `` is your language name. For example, `feat/translate-da`. +*** -4. Translation files are under `packages/admin/dashboard/src/i18n/translations` as JSON files whose names are the ISO-2 name of the language. - - If you're adding a new language, copy the file `packages/admin/dashboard/src/i18n/translations/en.json` and paste it with the ISO-2 name for your language. For example, if you're adding Danish translations, copy the `en.json` file and paste it as `packages/admin/dashboard/src/i18n/translations/de.json`. - - If you're fixing a translation, find the JSON file of the language under `packages/admin/dashboard/src/i18n/translations`. +### Run Tests -5. Start translating the keys in the JSON file (or updating the targeted ones). All keys in the JSON file must be translated, and your PR tests will fail otherwise. - - You can check whether the JSON file is valid by running the following command in `packages/admin/dashboard`, replacing `da.json` with the JSON file's name: +Run the following command to run your tests: -```bash title="packages/admin/dashboard" -yarn i18n:validate da.json +```bash npm2yarn +npm run test:integration ``` -6. After finishing the translation, if you're adding a new language, import its JSON file in `packages/admin/dashboard/src/i18n/translations/index.ts` and add it to the exported object: +If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). -```ts title="packages/admin/dashboard/src/i18n/translations/index.ts" highlights={[["2"], ["6"], ["7"], ["8"]]} -// other imports... -import da from "./da.json" +This runs your Medusa application and runs the tests available under the `src/integrations/http` directory. -export default { - // other languages... - da: { - translation: da, - }, -} -``` +*** -The language's key in the object is the ISO-2 name of the language. +## Other Options and Inputs -7. If you're adding a new language, add it to the file `packages/admin/dashboard/src/i18n/languages.ts`: +Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. -```ts title="packages/admin/dashboard/src/i18n/languages.ts" highlights={languageHighlights} -import { da } from "date-fns/locale" -// other imports... +*** -export const languages: Language[] = [ - // other languages... - { - code: "da", - display_name: "Danish", - ltr: true, - date_locale: da, - }, -] -``` +## Database Used in Tests -`languages` is an array having the following properties: +The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. -- `code`: The ISO-2 name of the language. For example, `da` for Danish. -- `display_name`: The language's name to be displayed in the admin. -- `ltr`: Whether the language supports a left-to-right layout. For example, set this to `false` for languages like Arabic. -- `date_locale`: An instance of the locale imported from the [date-fns/locale](https://date-fns.org/) package. +To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md). -8. Once you're done, push the changes into your branch and open a pull request on GitHub. +*** -Our team will perform a general review on your PR and merge it if no issues are found. The translation will be available in the admin after the next release. +## Example Integration Tests + +The next chapters provide examples of writing integration tests for API routes and workflows. -# Example: Integration Tests for a Module +# Write Tests for Modules -In this chapter, find an example of writing an integration test for a module using [moduleIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/modules-tests/index.html.md) from Medusa's Testing Framework. +In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests for a module's main service. ### Prerequisites - [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -## 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!" - } -} +## moduleIntegrationTestRunner Utility -export default HelloModuleService -``` +`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. -To create an integration test for the method, create the file `src/modules/hello/__tests__/service.spec.ts` with the following content: +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 "@medusajs/test-utils" @@ -15511,24 +15377,29 @@ moduleIntegrationTestRunner({ moduleModels: [MyCustom], resolve: "./src/modules/hello", testSuite: ({ service }) => { - describe("HelloModuleService", () => { - it("says hello world", () => { - const message = service.getMessage() - - expect(message).toEqual("Hello, World!") - }) - }) + // TODO write tests }, }) jest.setTimeout(60 * 1000) ``` -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. +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. + +The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property. + +The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). *** -## Run Test +## Run Tests Run the following command to run your module integration tests: @@ -15540,6 +15411,65 @@ If you don't have a `test:integration:modules` script in `package.json`, refer t 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 "@medusajs/test-utils" +import HelloModuleService from "../service" + +moduleIntegrationTestRunner({ + 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 "@medusajs/test-utils" +import HelloModuleService from "../service" +import { model } from "@medusajs/framework/utils" + +const DummyModel = model.define("dummy_model", { + id: model.id().primaryKey(), +}) + +moduleIntegrationTestRunner({ + moduleModels: [DummyModel], + // ... +}) + +jest.setTimeout(60 * 1000) +``` + +*** + +### Other Options and Inputs + +Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) 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 Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). + # Example: Write Integration Tests for API Routes @@ -16238,6 +16168,76 @@ The `errors` property contains an array of errors thrown during the execution of If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. +# Example: Integration Tests for a Module + +In this chapter, find an example of writing an integration test for a module using [moduleIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/modules-tests/index.html.md) from Medusa's Testing Framework. + +### Prerequisites + +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) + +## 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 "@medusajs/test-utils" +import { HELLO_MODULE } from ".." +import HelloModuleService from "../service" +import MyCustom from "../models/my-custom" + +moduleIntegrationTestRunner({ + 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!") + }) + }) + }, +}) + +jest.setTimeout(60 * 1000) +``` + +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:integration:modules +``` + +If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). + +This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. + + # Commerce Modules In this section of the documentation, you'll find guides and references related to Medusa's commerce modules. @@ -16418,24 +16418,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Currency Module - -In this section of the documentation, you will find resources to learn more about the Currency Module and how to use it in your application. +# Auth Module -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store's currencies using the dashboard. +In this section of the documentation, you will find resources to learn more about the Auth Module and how to use it in your application. -Medusa has currency related features available out-of-the-box through the Currency Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Currency Module. +Medusa has auth related features available out-of-the-box through the Auth Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Auth Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Currency Features +## Auth Features -- [Currency Management and Retrieval](https://docs.medusajs.com/references/currency/listAndCountCurrencies/index.html.md): This module adds all common currencies to your application and allows you to retrieve them. -- [Support Currencies in Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/links-to-other-modules/index.html.md): Other commerce modules use currency codes in their data models or operations. Use the Currency Module to retrieve a currency code and its details. +- [Basic User Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#1-basic-authentication-flow/index.html.md): Authenticate users using their email and password credentials. +- [Third-Party and Social Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md): Authenticate users using third-party services and social platforms, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) and [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md). +- [Authenticate Custom Actor Types](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md): Create custom user or actor types, such as managers, authenticate them in your application, and guard routes based on the custom user types. +- [Custom Authentication Providers](https://docs.medusajs.com/references/auth/provider/index.html.md): Integrate third-party services with custom authentication providors. *** -## How to Use the Currency Module +## How to Use the Auth Module In your Medusa application, you build flows around commerce modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -16443,147 +16443,129 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/retrieve-price-with-currency.ts" highlights={highlights} +```ts title="src/workflows/authenticate-user.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, createStep, StepResponse, - transform, } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const retrieveCurrencyStep = createStep( - "retrieve-currency", - async ({}, { container }) => { - const currencyModuleService = container.resolve(Modules.CURRENCY) - - const currency = await currencyModuleService - .retrieveCurrency("usd") - - return new StepResponse({ currency }) - } -) +import { Modules, MedusaError } from "@medusajs/framework/utils" +import { MedusaRequest } from "@medusajs/framework/http" +import { AuthenticationInput } from "@medusajs/framework/types" type Input = { - price: number + req: MedusaRequest } -export const retrievePriceWithCurrency = createWorkflow( - "create-currency", - (input: Input) => { - const { currency } = retrieveCurrencyStep() - - const formattedPrice = transform({ - input, - currency, - }, (data) => { - return `${data.currency.symbol}${data.input.price}` - }) - - return new WorkflowResponse({ - formattedPrice, - }) - } -) -``` +const authenticateUserStep = createStep( + "authenticate-user", + async ({ req }: Input, { container }) => { + const authModuleService = container.resolve(Modules.AUTH) -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + const { success, authIdentity, error } = await authModuleService + .authenticate( + "emailpass", + { + url: req.url, + headers: req.headers, + query: req.query, + body: req.body, + authScope: "admin", // or custom actor type + protocol: req.protocol, + } as AuthenticationInput + ) -### API Route + if (!success) { + // incorrect authentication details + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + error || "Incorrect authentication details" + ) + } -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" + return new StepResponse({ authIdentity }, authIdentity?.id) + }, + async (authIdentityId, { container }) => { + if (!authIdentityId) { + return + } + + const authModuleService = container.resolve(Modules.AUTH) + + await authModuleService.deleteAuthIdentities([authIdentityId]) + } +) + +export const authenticateUserWorkflow = createWorkflow( + "authenticate-user", + (input: Input) => { + const { authIdentity } = authenticateUserStep(input) + + return new WorkflowResponse({ + authIdentity, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +```ts title="API Route" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency" +import { authenticateUserWorkflow } from "../../workflows/authenticate-user" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await retrievePriceWithCurrency(req.scope) + const { result } = await authenticateUserWorkflow(req.scope) .run({ - price: 10, + req, }) res.send(result) } ``` -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await retrievePriceWithCurrency(container) - .run({ - price: 10, - }) - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -### Scheduled Job +*** -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" +## Configure Auth Module -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await retrievePriceWithCurrency(container) - .run({ - price: 10, - }) +The Auth Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/module-options/index.html.md) for details on the module's options. - console.log(result) -} +*** -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` +## Providers -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). +Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types. *** -# Cart Module +# Currency Module -In this section of the documentation, you will find resources to learn more about the Cart Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Currency Module and how to use it in your application. -Medusa has cart related features available out-of-the-box through the Cart Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Cart Module. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store's currencies using the dashboard. + +Medusa has currency related features available out-of-the-box through the Currency Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Currency Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Cart Features +## Currency Features -- [Cart Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/concepts/index.html.md): Store and manage carts, including their addresses, line items, shipping methods, and more. -- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/promotions/index.html.md): Apply promotions or discounts to line items and shipping methods by adding adjustment lines that are factored into their subtotals. -- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md): Apply tax lines to line items and shipping methods. -- [Cart Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/index.html.md): When used in the Medusa application, Medusa creates links to other commerce modules, scoping a cart to a sales channel, region, and a customer. +- [Currency Management and Retrieval](https://docs.medusajs.com/references/currency/listAndCountCurrencies/index.html.md): This module adds all common currencies to your application and allows you to retrieve them. +- [Support Currencies in Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/links-to-other-modules/index.html.md): Other commerce modules use currency codes in their data models or operations. Use the Currency Module to retrieve a currency code and its details. *** -## How to Use the Cart Module +## How to Use the Currency Module In your Medusa application, you build flows around commerce modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -16591,54 +16573,46 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-cart.ts" highlights={highlights} +```ts title="src/workflows/retrieve-price-with-currency.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, createStep, StepResponse, + transform, } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createCartStep = createStep( - "create-cart", +const retrieveCurrencyStep = createStep( + "retrieve-currency", async ({}, { container }) => { - const cartModuleService = container.resolve(Modules.CART) - - const cart = await cartModuleService.createCarts({ - currency_code: "usd", - shipping_address: { - address_1: "1512 Barataria Blvd", - country_code: "us", - }, - items: [ - { - title: "Shirt", - unit_price: 1000, - quantity: 1, - }, - ], - }) + const currencyModuleService = container.resolve(Modules.CURRENCY) - return new StepResponse({ cart }, cart.id) - }, - async (cartId, { container }) => { - if (!cartId) { - return - } - const cartModuleService = container.resolve(Modules.CART) + const currency = await currencyModuleService + .retrieveCurrency("usd") - await cartModuleService.deleteCarts([cartId]) + return new StepResponse({ currency }) } ) -export const createCartWorkflow = createWorkflow( - "create-cart", - () => { - const { cart } = createCartStep() +type Input = { + price: number +} + +export const retrievePriceWithCurrency = createWorkflow( + "create-currency", + (input: Input) => { + const { currency } = retrieveCurrencyStep() + + const formattedPrice = transform({ + input, + currency, + }, (data) => { + return `${data.currency.symbol}${data.input.price}` + }) return new WorkflowResponse({ - cart, + formattedPrice, }) } ) @@ -16648,19 +16622,21 @@ You can then execute the workflow in your custom API routes, scheduled jobs, or ### API Route -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createCartWorkflow } from "../../workflows/create-cart" +import { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createCartWorkflow(req.scope) - .run() + const { result } = await retrievePriceWithCurrency(req.scope) + .run({ + price: 10, + }) res.send(result) } @@ -16668,19 +16644,21 @@ export async function GET( ### Subscriber -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createCartWorkflow } from "../workflows/create-cart" +import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createCartWorkflow(container) - .run() + const { result } = await retrievePriceWithCurrency(container) + .run({ + price: 10, + }) console.log(result) } @@ -16692,15 +16670,17 @@ export const config: SubscriberConfig = { ### Scheduled Job -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createCartWorkflow } from "../workflows/create-cart" +import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createCartWorkflow(container) - .run() + const { result } = await retrievePriceWithCurrency(container) + .run({ + price: 10, + }) console.log(result) } @@ -16857,24 +16837,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Auth Module +# Cart Module -In this section of the documentation, you will find resources to learn more about the Auth Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Cart Module and how to use it in your application. -Medusa has auth related features available out-of-the-box through the Auth Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Auth Module. +Medusa has cart related features available out-of-the-box through the Cart Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Cart Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Auth Features +## Cart Features -- [Basic User Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#1-basic-authentication-flow/index.html.md): Authenticate users using their email and password credentials. -- [Third-Party and Social Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md): Authenticate users using third-party services and social platforms, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) and [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md). -- [Authenticate Custom Actor Types](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md): Create custom user or actor types, such as managers, authenticate them in your application, and guard routes based on the custom user types. -- [Custom Authentication Providers](https://docs.medusajs.com/references/auth/provider/index.html.md): Integrate third-party services with custom authentication providors. +- [Cart Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/concepts/index.html.md): Store and manage carts, including their addresses, line items, shipping methods, and more. +- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/promotions/index.html.md): Apply promotions or discounts to line items and shipping methods by adding adjustment lines that are factored into their subtotals. +- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md): Apply tax lines to line items and shipping methods. +- [Cart Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/index.html.md): When used in the Medusa application, Medusa creates links to other commerce modules, scoping a cart to a sales channel, region, and a customer. *** -## How to Use the Auth Module +## How to Use the Cart Module In your Medusa application, you build flows around commerce modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -16882,67 +16862,54 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/authenticate-user.ts" highlights={highlights} +```ts title="src/workflows/create-cart.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, createStep, StepResponse, } from "@medusajs/framework/workflows-sdk" -import { Modules, MedusaError } from "@medusajs/framework/utils" -import { MedusaRequest } from "@medusajs/framework/http" -import { AuthenticationInput } from "@medusajs/framework/types" - -type Input = { - req: MedusaRequest -} +import { Modules } from "@medusajs/framework/utils" -const authenticateUserStep = createStep( - "authenticate-user", - async ({ req }: Input, { container }) => { - const authModuleService = container.resolve(Modules.AUTH) +const createCartStep = createStep( + "create-cart", + async ({}, { container }) => { + const cartModuleService = container.resolve(Modules.CART) - const { success, authIdentity, error } = await authModuleService - .authenticate( - "emailpass", - { - url: req.url, - headers: req.headers, - query: req.query, - body: req.body, - authScope: "admin", // or custom actor type - protocol: req.protocol, - } as AuthenticationInput - ) - - if (!success) { - // incorrect authentication details - throw new MedusaError( - MedusaError.Types.UNAUTHORIZED, - error || "Incorrect authentication details" - ) - } + const cart = await cartModuleService.createCarts({ + currency_code: "usd", + shipping_address: { + address_1: "1512 Barataria Blvd", + country_code: "us", + }, + items: [ + { + title: "Shirt", + unit_price: 1000, + quantity: 1, + }, + ], + }) - return new StepResponse({ authIdentity }, authIdentity?.id) + return new StepResponse({ cart }, cart.id) }, - async (authIdentityId, { container }) => { - if (!authIdentityId) { + async (cartId, { container }) => { + if (!cartId) { return } - - const authModuleService = container.resolve(Modules.AUTH) + const cartModuleService = container.resolve(Modules.CART) - await authModuleService.deleteAuthIdentities([authIdentityId]) + await cartModuleService.deleteCarts([cartId]) } ) -export const authenticateUserWorkflow = createWorkflow( - "authenticate-user", - (input: Input) => { - const { authIdentity } = authenticateUserStep(input) +export const createCartWorkflow = createWorkflow( + "create-cart", + () => { + const { cart } = createCartStep() return new WorkflowResponse({ - authIdentity, + cart, }) } ) @@ -16950,39 +16917,72 @@ export const authenticateUserWorkflow = createWorkflow( You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -```ts title="API Route" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { authenticateUserWorkflow } from "../../workflows/authenticate-user" +import { createCartWorkflow } from "../../workflows/create-cart" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await authenticateUserWorkflow(req.scope) - .run({ - req, - }) + const { result } = await createCartWorkflow(req.scope) + .run() res.send(result) } ``` -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). +### Subscriber -*** +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createCartWorkflow } from "../workflows/create-cart" -## Configure Auth Module +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createCartWorkflow(container) + .run() -The Auth Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/module-options/index.html.md) for details on the module's options. + console.log(result) +} -*** +export const config: SubscriberConfig = { + event: "user.created", +} +``` -## Providers +### Scheduled Job -Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types. +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createCartWorkflow } from "../workflows/create-cart" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createCartWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). *** @@ -17453,27 +17453,27 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Pricing Module +# Payment Module -In this section of the documentation, you will find resources to learn more about the Pricing Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Payment Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/price-lists/index.html.md) to learn how to manage price lists using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/payments/index.html.md) to learn how to manage order payments using the dashboard. -Medusa has pricing related features available out-of-the-box through the Pricing Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Pricing Module. +Medusa has payment related features available out-of-the-box through the Payment Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Payment Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Pricing Features +## Payment Features -- [Price Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts/index.html.md): Store and manage prices of a resource, such as a product or a variant. -- [Advanced Rule Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Create prices with custom rules to condition prices based on different contexts. -- [Price Lists](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts#price-list/index.html.md): Group prices and apply them only in specific conditions with price lists. -- [Price Calculation Strategy](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md): Retrieve the best price in a given context and for the specified rule values. -- [Tax-Inclusive Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md): Calculate prices with taxes included in the price, and Medusa will handle calculating the taxes automatically. +- [Authorize, Capture, and Refund Payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md): Authorize, capture, and refund payments for a single resource. +- [Payment Collection Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection/index.html.md): Store and manage all payments of a single resources, such as a cart, in payment collections. +- [Integrate Third-Party Payment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md): Use payment providers like [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to handle and process payments, or integrate custom payment providers. +- [Saved Payment Methods](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md): Save payment methods for customers in third-party payment providers. +- [Handle Webhook Events](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/webhook-events/index.html.md): Handle webhook events from third-party providers and process the associated payment. *** -## How to Use the Pricing Module +## How to Use the Payment Module In your Medusa application, you build flows around commerce modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -17481,7 +17481,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-price-set.ts" highlights={highlights} +```ts title="src/workflows/create-payment-collection.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -17490,46 +17490,35 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createPriceSetStep = createStep( - "create-price-set", +const createPaymentCollectionStep = createStep( + "create-payment-collection", async ({}, { container }) => { - const pricingModuleService = container.resolve(Modules.PRICING) + const paymentModuleService = container.resolve(Modules.PAYMENT) - const priceSet = await pricingModuleService.createPriceSets({ - prices: [ - { - amount: 500, - currency_code: "USD", - }, - { - amount: 400, - currency_code: "EUR", - min_quantity: 0, - max_quantity: 4, - rules: {}, - }, - ], + const paymentCollection = await paymentModuleService.createPaymentCollections({ + currency_code: "usd", + amount: 5000, }) - return new StepResponse({ priceSet }, priceSet.id) + return new StepResponse({ paymentCollection }, paymentCollection.id) }, - async (priceSetId, { container }) => { - if (!priceSetId) { + async (paymentCollectionId, { container }) => { + if (!paymentCollectionId) { return } - const pricingModuleService = container.resolve(Modules.PRICING) + const paymentModuleService = container.resolve(Modules.PAYMENT) - await pricingModuleService.deletePriceSets([priceSetId]) + await paymentModuleService.deletePaymentCollections([paymentCollectionId]) } ) -export const createPriceSetWorkflow = createWorkflow( - "create-price-set", +export const createPaymentCollectionWorkflow = createWorkflow( + "create-payment-collection", () => { - const { priceSet } = createPriceSetStep() + const { paymentCollection } = createPaymentCollectionStep() return new WorkflowResponse({ - priceSet, + paymentCollection, }) } ) @@ -17544,13 +17533,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createPriceSetWorkflow } from "../../workflows/create-price-set" +import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createPriceSetWorkflow(req.scope) + const { result } = await createPaymentCollectionWorkflow(req.scope) .run() res.send(result) @@ -17564,13 +17553,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createPriceSetWorkflow } from "../workflows/create-price-set" +import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createPriceSetWorkflow(container) + const { result } = await createPaymentCollectionWorkflow(container) .run() console.log(result) @@ -17585,12 +17574,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createPriceSetWorkflow } from "../workflows/create-price-set" +import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createPriceSetWorkflow(container) + const { result } = await createPaymentCollectionWorkflow(container) .run() console.log(result) @@ -17606,28 +17595,40 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +## Configure Payment Module -# Payment Module +The Payment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) for details on the module's options. -In this section of the documentation, you will find resources to learn more about the Payment Module and how to use it in your application. +*** -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/payments/index.html.md) to learn how to manage order payments using the dashboard. +## Providers -Medusa has payment related features available out-of-the-box through the Payment Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Payment Module. +Medusa provides the following payment providers out-of-the-box. You can use them to process payments for orders, returns, and other resources. + +*** + + +# Pricing Module + +In this section of the documentation, you will find resources to learn more about the Pricing Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/price-lists/index.html.md) to learn how to manage price lists using the dashboard. + +Medusa has pricing related features available out-of-the-box through the Pricing Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Pricing Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Payment Features +## Pricing Features -- [Authorize, Capture, and Refund Payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md): Authorize, capture, and refund payments for a single resource. -- [Payment Collection Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection/index.html.md): Store and manage all payments of a single resources, such as a cart, in payment collections. -- [Integrate Third-Party Payment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md): Use payment providers like [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to handle and process payments, or integrate custom payment providers. -- [Saved Payment Methods](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md): Save payment methods for customers in third-party payment providers. -- [Handle Webhook Events](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/webhook-events/index.html.md): Handle webhook events from third-party providers and process the associated payment. +- [Price Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts/index.html.md): Store and manage prices of a resource, such as a product or a variant. +- [Advanced Rule Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Create prices with custom rules to condition prices based on different contexts. +- [Price Lists](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts#price-list/index.html.md): Group prices and apply them only in specific conditions with price lists. +- [Price Calculation Strategy](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md): Retrieve the best price in a given context and for the specified rule values. +- [Tax-Inclusive Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md): Calculate prices with taxes included in the price, and Medusa will handle calculating the taxes automatically. *** -## How to Use the Payment Module +## How to Use the Pricing Module In your Medusa application, you build flows around commerce modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -17635,7 +17636,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-payment-collection.ts" highlights={highlights} +```ts title="src/workflows/create-price-set.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -17644,35 +17645,46 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createPaymentCollectionStep = createStep( - "create-payment-collection", +const createPriceSetStep = createStep( + "create-price-set", async ({}, { container }) => { - const paymentModuleService = container.resolve(Modules.PAYMENT) + const pricingModuleService = container.resolve(Modules.PRICING) - const paymentCollection = await paymentModuleService.createPaymentCollections({ - currency_code: "usd", - amount: 5000, + const priceSet = await pricingModuleService.createPriceSets({ + prices: [ + { + amount: 500, + currency_code: "USD", + }, + { + amount: 400, + currency_code: "EUR", + min_quantity: 0, + max_quantity: 4, + rules: {}, + }, + ], }) - return new StepResponse({ paymentCollection }, paymentCollection.id) + return new StepResponse({ priceSet }, priceSet.id) }, - async (paymentCollectionId, { container }) => { - if (!paymentCollectionId) { + async (priceSetId, { container }) => { + if (!priceSetId) { return } - const paymentModuleService = container.resolve(Modules.PAYMENT) + const pricingModuleService = container.resolve(Modules.PRICING) - await paymentModuleService.deletePaymentCollections([paymentCollectionId]) + await pricingModuleService.deletePriceSets([priceSetId]) } ) -export const createPaymentCollectionWorkflow = createWorkflow( - "create-payment-collection", +export const createPriceSetWorkflow = createWorkflow( + "create-price-set", () => { - const { paymentCollection } = createPaymentCollectionStep() + const { priceSet } = createPriceSetStep() return new WorkflowResponse({ - paymentCollection, + priceSet, }) } ) @@ -17687,13 +17699,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection" +import { createPriceSetWorkflow } from "../../workflows/create-price-set" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createPaymentCollectionWorkflow(req.scope) + const { result } = await createPriceSetWorkflow(req.scope) .run() res.send(result) @@ -17707,13 +17719,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" +import { createPriceSetWorkflow } from "../workflows/create-price-set" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createPaymentCollectionWorkflow(container) + const { result } = await createPriceSetWorkflow(container) .run() console.log(result) @@ -17728,12 +17740,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" +import { createPriceSetWorkflow } from "../workflows/create-price-set" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createPaymentCollectionWorkflow(container) + const { result } = await createPriceSetWorkflow(container) .run() console.log(result) @@ -17749,18 +17761,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -## Configure Payment Module - -The Payment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) for details on the module's options. - -*** - -## Providers - -Medusa provides the following payment providers out-of-the-box. You can use them to process payments for orders, returns, and other resources. - -*** - # Region Module @@ -18207,24 +18207,38 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Stock Location Module +# Sales Channel Module -In this section of the documentation, you will find resources to learn more about the Stock Location Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Sales Channel Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md) to learn how to manage stock locations using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/sales-channels/index.html.md) to learn how to manage sales channels using the dashboard. -Medusa has stock location related features available out-of-the-box through the Stock Location Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Stock Location Module. +Medusa has sales channel related features available out-of-the-box through the Sales Channel Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Sales Channel Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Stock Location Features +## What's a Sales Channel? -- [Stock Location Management](https://docs.medusajs.com/references/stock-location-next/models/index.html.md): Store and manage stock locations. Medusa links stock locations with data models of other modules that require a location, such as the [Inventory Module's InventoryLevel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/links-to-other-modules/index.html.md). -- [Address Management](https://docs.medusajs.com/references/stock-location-next/models/StockLocationAddress/index.html.md): Manage the address of each stock location. +A sales channel indicates an online or offline channel that you sell products on. + +Some use case examples for using a sales channel: + +- Implement a B2B Ecommerce Store. +- Specify different products for each channel you sell in. +- Support omnichannel in your ecommerce store. *** -## How to Use Stock Location Module's Service +## Sales Channel Features + +- [Sales Channel Management](https://docs.medusajs.com/references/sales-channel/models/SalesChannel/index.html.md): Manage sales channels in your store. Each sales channel has different meta information such as name or description, allowing you to easily differentiate between sales channels. +- [Product Availability](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa uses the Product and Sales Channel modules to allow merchants to specify a product's availability per sales channel. +- [Cart and Order Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Carts, available through the Cart Module, are scoped to a sales channel. Paired with the product availability feature, you benefit from more features like allowing only products available in sales channel in a cart. +- [Inventory Availability Per Sales Channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa links sales channels to stock locations, allowing you to retrieve available inventory of products based on the specified sales channel. + +*** + +## How to Use Sales Channel Module's Service In your Medusa application, you build flows around commerce modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -18232,7 +18246,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-stock-location.ts" highlights={highlights} +```ts title="src/workflows/create-sales-channel.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -18241,33 +18255,42 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createStockLocationStep = createStep( - "create-stock-location", +const createSalesChannelStep = createStep( + "create-sales-channel", async ({}, { container }) => { - const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) - const stockLocation = await stockLocationModuleService.createStockLocations({ - name: "Warehouse 1", - }) + const salesChannels = await salesChannelModuleService.createSalesChannels([ + { + name: "B2B", + }, + { + name: "Mobile App", + }, + ]) - return new StepResponse({ stockLocation }, stockLocation.id) + return new StepResponse({ salesChannels }, salesChannels.map((sc) => sc.id)) }, - async (stockLocationId, { container }) => { - if (!stockLocationId) { + async (salesChannelIds, { container }) => { + if (!salesChannelIds) { return } - const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) - await stockLocationModuleService.deleteStockLocations([stockLocationId]) + await salesChannelModuleService.deleteSalesChannels( + salesChannelIds + ) } ) -export const createStockLocationWorkflow = createWorkflow( - "create-stock-location", +export const createSalesChannelWorkflow = createWorkflow( + "create-sales-channel", () => { - const { stockLocation } = createStockLocationStep() + const { salesChannels } = createSalesChannelStep() - return new WorkflowResponse({ stockLocation }) + return new WorkflowResponse({ + salesChannels, + }) } ) ``` @@ -18281,13 +18304,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createStockLocationWorkflow } from "../../workflows/create-stock-location" +import { createSalesChannelWorkflow } from "../../workflows/create-sales-channel" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createStockLocationWorkflow(req.scope) + const { result } = await createSalesChannelWorkflow(req.scope) .run() res.send(result) @@ -18301,13 +18324,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createStockLocationWorkflow } from "../workflows/create-stock-location" +import { createSalesChannelWorkflow } from "../workflows/create-sales-channel" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createStockLocationWorkflow(container) + const { result } = await createSalesChannelWorkflow(container) .run() console.log(result) @@ -18322,12 +18345,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createStockLocationWorkflow } from "../workflows/create-stock-location" +import { createSalesChannelWorkflow } from "../workflows/create-sales-channel" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createStockLocationWorkflow(container) + const { result } = await createSalesChannelWorkflow(container) .run() console.log(result) @@ -18344,38 +18367,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Sales Channel Module +# Stock Location Module -In this section of the documentation, you will find resources to learn more about the Sales Channel Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Stock Location Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/sales-channels/index.html.md) to learn how to manage sales channels using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md) to learn how to manage stock locations using the dashboard. -Medusa has sales channel related features available out-of-the-box through the Sales Channel Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Sales Channel Module. +Medusa has stock location related features available out-of-the-box through the Stock Location Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Stock Location Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## What's a Sales Channel? - -A sales channel indicates an online or offline channel that you sell products on. - -Some use case examples for using a sales channel: - -- Implement a B2B Ecommerce Store. -- Specify different products for each channel you sell in. -- Support omnichannel in your ecommerce store. - -*** - -## Sales Channel Features +## Stock Location Features -- [Sales Channel Management](https://docs.medusajs.com/references/sales-channel/models/SalesChannel/index.html.md): Manage sales channels in your store. Each sales channel has different meta information such as name or description, allowing you to easily differentiate between sales channels. -- [Product Availability](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa uses the Product and Sales Channel modules to allow merchants to specify a product's availability per sales channel. -- [Cart and Order Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Carts, available through the Cart Module, are scoped to a sales channel. Paired with the product availability feature, you benefit from more features like allowing only products available in sales channel in a cart. -- [Inventory Availability Per Sales Channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa links sales channels to stock locations, allowing you to retrieve available inventory of products based on the specified sales channel. +- [Stock Location Management](https://docs.medusajs.com/references/stock-location-next/models/index.html.md): Store and manage stock locations. Medusa links stock locations with data models of other modules that require a location, such as the [Inventory Module's InventoryLevel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/links-to-other-modules/index.html.md). +- [Address Management](https://docs.medusajs.com/references/stock-location-next/models/StockLocationAddress/index.html.md): Manage the address of each stock location. *** -## How to Use Sales Channel Module's Service +## How to Use Stock Location Module's Service In your Medusa application, you build flows around commerce modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -18383,7 +18392,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-sales-channel.ts" highlights={highlights} +```ts title="src/workflows/create-stock-location.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -18392,42 +18401,33 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createSalesChannelStep = createStep( - "create-sales-channel", +const createStockLocationStep = createStep( + "create-stock-location", async ({}, { container }) => { - const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) + const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) - const salesChannels = await salesChannelModuleService.createSalesChannels([ - { - name: "B2B", - }, - { - name: "Mobile App", - }, - ]) + const stockLocation = await stockLocationModuleService.createStockLocations({ + name: "Warehouse 1", + }) - return new StepResponse({ salesChannels }, salesChannels.map((sc) => sc.id)) + return new StepResponse({ stockLocation }, stockLocation.id) }, - async (salesChannelIds, { container }) => { - if (!salesChannelIds) { + async (stockLocationId, { container }) => { + if (!stockLocationId) { return } - const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) + const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) - await salesChannelModuleService.deleteSalesChannels( - salesChannelIds - ) + await stockLocationModuleService.deleteStockLocations([stockLocationId]) } ) -export const createSalesChannelWorkflow = createWorkflow( - "create-sales-channel", +export const createStockLocationWorkflow = createWorkflow( + "create-stock-location", () => { - const { salesChannels } = createSalesChannelStep() + const { stockLocation } = createStockLocationStep() - return new WorkflowResponse({ - salesChannels, - }) + return new WorkflowResponse({ stockLocation }) } ) ``` @@ -18441,13 +18441,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createSalesChannelWorkflow } from "../../workflows/create-sales-channel" +import { createStockLocationWorkflow } from "../../workflows/create-stock-location" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createSalesChannelWorkflow(req.scope) + const { result } = await createStockLocationWorkflow(req.scope) .run() res.send(result) @@ -18461,13 +18461,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createSalesChannelWorkflow } from "../workflows/create-sales-channel" +import { createStockLocationWorkflow } from "../workflows/create-stock-location" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createSalesChannelWorkflow(container) + const { result } = await createStockLocationWorkflow(container) .run() console.log(result) @@ -18482,12 +18482,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createSalesChannelWorkflow } from "../workflows/create-sales-channel" +import { createStockLocationWorkflow } from "../workflows/create-stock-location" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createSalesChannelWorkflow(container) + const { result } = await createStockLocationWorkflow(container) .run() console.log(result) @@ -18792,190 +18792,66 @@ The User Module accepts options for further configurations. Refer to [this docum *** -# API Key Concepts - -In this document, you’ll learn about the different types of API keys, their expiration and verification. - -## API Key Types - -There are two types of API keys: +# Tax Module -- `publishable`: A public key used in client applications, such as a storefront. -- `secret`: A secret key used for authentication and verification purposes, such as an admin user’s authentication token or a password reset token. +In this section of the documentation, you will find resources to learn more about the Tax Module and how to use it in your application. -The API key’s type is stored in the `type` property of the [ApiKey data model](https://docs.medusajs.com/references/api-key/models/ApiKey/index.html.md). +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. -*** +Medusa has tax related features available out-of-the-box through the Tax Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Tax Module. -## API Key Expiration +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -An API key expires when it’s revoked using the [revoke method of the module’s main service](https://docs.medusajs.com/references/api-key/revoke/index.html.md). +## Tax Features -The associated token is no longer usable or verifiable. +- [Tax Settings Per Region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md): Set different tax settings for each tax region. +- [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md): Manage each region's default tax rates and override them with conditioned tax rates. +- [Retrieve Tax Lines for carts and orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md): Calculate and retrieve the tax lines of a cart or order's line items and shipping methods with tax providers. *** -## Token Verification +## How to Use Tax Module's Service -To verify a token received as an input or in a request, use the [authenticate method of the module’s main service](https://docs.medusajs.com/references/api-key/authenticate/index.html.md) which validates the token against all non-expired tokens. +In your Medusa application, you build flows around commerce modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. -# Links between API Key Module and Other Modules +For example: -This document showcases the module links defined between the API Key Module and other commerce modules. +```ts title="src/workflows/create-tax-region.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" -## Summary +const createTaxRegionStep = createStep( + "create-tax-region", + async ({}, { container }) => { + const taxModuleService = container.resolve(Modules.TAX) -The API Key Module has the following links to other modules: + const taxRegion = await taxModuleService.createTaxRegions({ + country_code: "us", + }) -- [`ApiKey` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). + return new StepResponse({ taxRegion }, taxRegion.id) + }, + async (taxRegionId, { container }) => { + if (!taxRegionId) { + return + } + const taxModuleService = container.resolve(Modules.TAX) -*** + await taxModuleService.deleteTaxRegions([taxRegionId]) + } +) -## Sales Channel Module - -You can create a publishable API key and associate it with a sales channel. Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. - -![A diagram showcasing an example of how data models from the API Key and Sales Channel modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) - -This is useful to avoid passing the sales channel's ID as a parameter of every request, and instead pass the publishable API key in the header of any request to the Store API route. - -Learn more about this in the [Sales Channel Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). - -### Retrieve with Query - -To retrieve the sales channels of an API key with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: - -### query.graph - -```ts -const { data: apiKeys } = await query.graph({ - entity: "api_key", - fields: [ - "sales_channels.*", - ], -}) - -// apiKeys.sales_channels -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: apiKeys } = useQueryGraphStep({ - entity: "api_key", - fields: [ - "sales_channels.*", - ], -}) - -// apiKeys.sales_channels -``` - -### Manage with Link - -To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.API_KEY]: { - api_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.API_KEY]: { - api_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` - - -# Tax Module - -In this section of the documentation, you will find resources to learn more about the Tax Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. - -Medusa has tax related features available out-of-the-box through the Tax Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Tax Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Tax Features - -- [Tax Settings Per Region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md): Set different tax settings for each tax region. -- [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md): Manage each region's default tax rates and override them with conditioned tax rates. -- [Retrieve Tax Lines for carts and orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md): Calculate and retrieve the tax lines of a cart or order's line items and shipping methods with tax providers. - -*** - -## How to Use Tax Module's Service - -In your Medusa application, you build flows around commerce modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-tax-region.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createTaxRegionStep = createStep( - "create-tax-region", - async ({}, { container }) => { - const taxModuleService = container.resolve(Modules.TAX) - - const taxRegion = await taxModuleService.createTaxRegions({ - country_code: "us", - }) - - return new StepResponse({ taxRegion }, taxRegion.id) - }, - async (taxRegionId, { container }) => { - if (!taxRegionId) { - return - } - const taxModuleService = container.resolve(Modules.TAX) - - await taxModuleService.deleteTaxRegions([taxRegionId]) - } -) - -export const createTaxRegionWorkflow = createWorkflow( - "create-tax-region", - () => { - const { taxRegion } = createTaxRegionStep() +export const createTaxRegionWorkflow = createWorkflow( + "create-tax-region", + () => { + const { taxRegion } = createTaxRegionStep() return new WorkflowResponse({ taxRegion }) } @@ -19060,41 +18936,73 @@ The Tax Module accepts options for further configurations. Refer to [this docume *** -# Links between Currency Module and Other Modules +# API Key Concepts -This document showcases the module links defined between the Currency Module and other commerce modules. +In this document, you’ll learn about the different types of API keys, their expiration and verification. -## Summary +## API Key Types -The Currency Module has the following links to other modules: +There are two types of API keys: -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +- `publishable`: A public key used in client applications, such as a storefront. +- `secret`: A secret key used for authentication and verification purposes, such as an admin user’s authentication token or a password reset token. -- [`Currency` data model of Store Module \<> `Currency` data model of Currency Module](#store-module). (Read-only). +The API key’s type is stored in the `type` property of the [ApiKey data model](https://docs.medusajs.com/references/api-key/models/ApiKey/index.html.md). *** -## Store Module +## API Key Expiration -The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. +An API key expires when it’s revoked using the [revoke method of the module’s main service](https://docs.medusajs.com/references/api-key/revoke/index.html.md). + +The associated token is no longer usable or verifiable. + +*** + +## Token Verification + +To verify a token received as an input or in a request, use the [authenticate method of the module’s main service](https://docs.medusajs.com/references/api-key/authenticate/index.html.md) which validates the token against all non-expired tokens. + + +# Links between API Key Module and Other Modules + +This document showcases the module links defined between the API Key Module and other commerce modules. + +## Summary + +The API Key Module has the following links to other modules: -Instead, Medusa defines a read-only link between the Currency Module's `Currency` data model and the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `Currency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database. The currencies of a store are determined by the `currency_code` of the `Currency` data model in the Store Module. +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|| in |Stored|| + +*** + +## Sales Channel Module + +You can create a publishable API key and associate it with a sales channel. Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. + +![A diagram showcasing an example of how data models from the API Key and Sales Channel modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) + +This is useful to avoid passing the sales channel's ID as a parameter of every request, and instead pass the publishable API key in the header of any request to the Store API route. + +Learn more about this in the [Sales Channel Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). ### Retrieve with Query -To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`: +To retrieve the sales channels of an API key with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: ### query.graph ```ts -const { data: stores } = await query.graph({ - entity: "store", +const { data: apiKeys } = await query.graph({ + entity: "api_key", fields: [ - "supported_currencies.currency.*", + "sales_channels.*", ], }) -// stores.supported_currencies +// apiKeys.sales_channels ``` ### useQueryGraphStep @@ -19104,2199 +19012,2326 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stores } = useQueryGraphStep({ - entity: "store", +const { data: apiKeys } = useQueryGraphStep({ + entity: "api_key", fields: [ - "supported_currencies.currency.*", + "sales_channels.*", ], }) -// stores.supported_currencies +// apiKeys.sales_channels ``` +### Manage with Link -# Cart Concepts +To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -In this document, you’ll get an overview of the main concepts of a cart. +### link.create -## Shipping and Billing Addresses +```ts +import { Modules } from "@medusajs/framework/utils" -A cart has a shipping and billing address. Both of these addresses are represented by the [Address data model](https://docs.medusajs.com/references/cart/models/Address/index.html.md). +// ... -![A diagram showcasing the relation between the Cart and Address data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711532392/Medusa%20Resources/cart-addresses_ls6qmv.jpg) +await link.create({ + [Modules.API_KEY]: { + api_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` -*** +### createRemoteLinkStep -## Line Items +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -A line item, represented by the [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model, is a quantity of a product variant added to the cart. A cart has multiple line items. +// ... -A line item stores some of the product variant’s properties, such as the `product_title` and `product_description`. It also stores data related to the item’s quantity and price. +createRemoteLinkStep({ + [Modules.API_KEY]: { + api_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` -In the Medusa application, a product variant is implemented in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). -*** +# Auth Identity and Actor Types -## Shipping Methods +In this document, you’ll learn about concepts related to identity and actors in the Auth Module. -A shipping method, represented by the [ShippingMethod data model](https://docs.medusajs.com/references/cart/models/ShippingMethod/index.html.md), is used to fulfill the items in the cart after the order is placed. A cart can have more than one shipping method. +## What is an Auth Identity? -In the Medusa application, the shipping method is created from a shipping option, available through the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md). Its ID is stored in the `shipping_option_id` property of the method. +The [AuthIdentity data model](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) represents a user registered by an [authentication provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). When a user is registered using an authentication provider, the provider creates a record of `AuthIdentity`. -### data Property - -After an order is placed, you can use a third-party fulfillment provider to fulfill its shipments. - -If the fulfillment provider requires additional custom data to be passed along from the checkout process, set this data in the `ShippingMethod`'s `data` property. - -The `data` property is an object used to store custom data relevant later for fulfillment. +Then, when the user logs-in in the future with the same authentication provider, the associated auth identity is used to validate their credentials. +*** -# Links between Cart Module and Other Modules +## Actor Types -This document showcases the module links defined between the Cart Module and other commerce modules. +An actor type is a type of user that can be authenticated. The Auth Module doesn't store or manage any user-like models, such as for customers or users. Instead, the user types are created and managed by other modules. For example, a customer is managed by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md). -## Summary +Then, when an auth identity is created for the actor type, the ID of the user is stored in the `app_metadata` property of the auth identity. -The Cart Module has the following links to other modules: +For example, an auth identity of a customer has the following `app_metadata` property: -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +```json +{ + "app_metadata": { + "customer_id": "cus_123" + } +} +``` -- [`Cart` data model \<> `Customer` data model of Customer Module](#customer-module). (Read-only). -- [`Order` data model of Order Module \<> `Cart` data model](#order-module). -- [`Cart` data model \<> `PaymentCollection` data model of Payment Module](#payment-module). -- [`LineItem` data model \<> `Product` data model of Product Module](#product-module). (Read-only). -- [`LineItem` data model \<> `ProductVariant` data model of Product Module](#product-module). (Read-only). -- [`Cart` data model \<> `Promotion` data model of Promotion Module](#promotion-module). -- [`Cart` data model \<> `Region` data model of Region Module](#region-module). (Read-only). -- [`Cart` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). (Read-only). +The ID of the user is stored in the key `{actor_type}_id` of the `app_metadata` property. *** -## Customer Module - -Medusa defines a read-only link between the `Cart` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of a cart's customer, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model. +## Protect Routes by Actor Type -### Retrieve with Query +When you protect routes with the `authenticate` middleware, you specify in its first parameter the actor type that must be authenticated to access the specified API routes. -To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: +For example: -### query.graph +```ts title="src/api/middlewares.ts" highlights={highlights} +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "customer.*", +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/admin*", + middlewares: [ + authenticate("user", ["session", "bearer", "api-key"]), + ], + }, ], }) - -// carts.order ``` -### useQueryGraphStep +By specifying `user` as the first parameter of `authenticate`, only authenticated users of actor type `user` (admin users) can access API routes starting with `/custom/admin`. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +*** -// ... +## Custom Actor Types -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "customer.*", - ], -}) +You can define custom actor types that allows a custom user, managed by your custom module, to authenticate into Medusa. -// carts.order -``` +For example, if you have a custom module with a `Manager` data model, you can authenticate managers with the `manager` actor type. -*** +Learn how to create a custom actor type in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md). -## Order Module -The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management features. +# Authentication Flows with the Auth Main Service -Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. +In this document, you'll learn how to use the Auth Module's main service's methods to implement authentication flows and reset a user's password. -![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg) +## Authentication Methods -### Retrieve with Query +### Register -To retrieve the order of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: +The [register method of the Auth Module's main service](https://docs.medusajs.com/references/auth/register/index.html.md) creates an auth identity that can be authenticated later. -### query.graph +For example: ```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "order.*", - ], -}) - -// carts.order +const data = await authModuleService.register( + "emailpass", + // passed to auth provider + { + // ... + } +) ``` -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +This method calls the `register` method of the provider specified in the first parameter and returns its data. -// ... +### Authenticate -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "order.*", - ], -}) +To authenticate a user, you use the [authenticate method of the Auth Module's main service](https://docs.medusajs.com/references/auth/authenticate/index.html.md). For example: -// carts.order +```ts +const data = await authModuleService.authenticate( + "emailpass", + // passed to auth provider + { + // ... + } +) ``` -### Manage with Link +This method calls the `authenticate` method of the provider specified in the first parameter and returns its data. -To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +*** -### link.create +## Auth Flow 1: Basic Authentication + +The basic authentication flow requires first using the `register` method, then the `authenticate` method: ```ts -import { Modules } from "@medusajs/framework/utils" +const { success, authIdentity, error } = await authModuleService.register( + "emailpass", + // passed to auth provider + { + // ... + } +) -// ... +if (error) { + // registration failed + // TODO return an error + return +} -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.ORDER]: { - order_id: "order_123", - }, -}) -``` +// later (can be another route for log-in) +const { success, authIdentity, location } = await authModuleService.authenticate( + "emailpass", + // passed to auth provider + { + // ... + } +) -### createRemoteLinkStep +if (success && !location) { + // user is authenticated +} +``` -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +If `success` is true and `location` isn't set, the user is authenticated successfully, and their authentication details are available within the `authIdentity` object. -// ... +The next section explains the flow if `location` is set. -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.ORDER]: { - order_id: "order_123", - }, -}) -``` +Check out the [AuthIdentity](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) reference for the received properties in `authIdentity`. -*** +![Diagram showcasing the basic authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711373749/Medusa%20Resources/basic-auth_lgpqsj.jpg) -## Payment Module +### Auth Identity with Same Identifier -The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) handles payment processing and management. +If an auth identity, such as a `customer`, tries to register with an email of another auth identity, the `register` method returns an error. This can happen either if another customer is using the same email, or an admin user has the same email. -Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. +There are two ways to handle this: -![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) +- Consider the customer authenticated if the `authenticate` method validates that the email and password are correct. This allows admin users, for example, to authenticate as customers. +- Return an error message to the customer, informing them that the email is already in use. -### Retrieve with Query +*** -To retrieve the payment collection of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collection.*` in `fields`: +## Auth Flow 2: Third-Party Service Authentication -### query.graph +The third-party service authentication method requires using the `authenticate` method first: ```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "payment_collection.*", - ], -}) +const { success, authIdentity, location } = await authModuleService.authenticate( + "google", + // passed to auth provider + { + // ... + } +) -// carts.payment_collection +if (location) { + // return the location for the front-end to redirect to +} + +if (!success) { + // authentication failed +} + +// authentication successful ``` -### useQueryGraphStep +If the `authenticate` method returns a `location` property, the authentication process requires the user to perform an action with a third-party service. So, you return the `location` to the front-end or client to redirect to that URL. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +For example, when using the `google` provider, the `location` is the URL that the user is navigated to login. -// ... +![Diagram showcasing the first part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711374847/Medusa%20Resources/third-party-auth-1_enyedy.jpg) -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "payment_collection.*", - ], -}) +### Overriding Callback URL -// carts.payment_collection +The Google and GitHub providers allow you to override their `callbackUrl` option during authentication. This is useful when you redirect the user after authentication to a URL based on its actor type. For example, you redirect admin users and customers to different pages. + +```ts +const { success, authIdentity, location } = await authModuleService.authenticate( + "google", + // passed to auth provider + { + // ... + callback_url: "example.com", + } +) ``` -### Manage with Link +### validateCallback -To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service. -### link.create +So, once the user performs the required action with the third-party service (for example, log-in with Google), the frontend must redirect to an API route that uses the [validateCallback method of the Auth Module's main service](https://docs.medusajs.com/references/auth/validateCallback/index.html.md). + +The method calls the specified provider’s `validateCallback` method passing it the authentication details it received in the second parameter: ```ts -import { Modules } from "@medusajs/framework/utils" - -// ... +const { success, authIdentity } = await authModuleService.validateCallback( + "google", + // passed to auth provider + { + // request data, such as + url, + headers, + query, + body, + protocol, + } +) -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) +if (success) { + // authentication succeeded +} ``` -### createRemoteLinkStep - -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +For providers like Google, the `query` object contains the query parameters from the original callback URL, such as the `code` and `state` parameters. -// ... +If the returned `success` property is `true`, the authentication with the third-party provider was successful. -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` +![Diagram showcasing the second part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711375123/Medusa%20Resources/third-party-auth-2_kmjxju.jpg) *** -## Product Module - -Medusa defines read-only links between: - -- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `LineItem` data model. -- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `LineItem` data model. - -### Retrieve with Query - -To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: +## Reset Password -To retrieve the product, pass `product.*` in `fields`. +To update a user's password or other authentication details, use the `updateProvider` method of the Auth Module's main service. It calls the `update` method of the specified authentication provider. -### query.graph +For example: ```ts -const { data: lineItems } = await query.graph({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) +const { success } = await authModuleService.updateProvider( + "emailpass", + // passed to the auth provider + { + entity_id: "user@example.com", + password: "supersecret", + } +) -// lineItems.variant +if (success) { + // password reset successfully +} ``` -### useQueryGraphStep +The method accepts as a first parameter the ID of the provider, and as a second parameter the data necessary to reset the password. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +In the example above, you use the `emailpass` provider, so you have to pass an object having an `email` and `password` properties. -// ... +If the returned `success` property is `true`, the password has reset successfully. -const { data: lineItems } = useQueryGraphStep({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) -// lineItems.variant -``` +# Auth Providers -*** +In this document, you’ll learn how the Auth Module handles authentication using providers. -## Promotion Module +## What's an Auth Module Provider? -The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features. +An auth module provider handles authenticating customers and users, either using custom logic or by integrating a third-party service. -Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart. +For example, the EmailPass Auth Module Provider authenticates a user using their email and password, whereas the Google Auth Module Provider authenticates users using their Google account. -![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) +### Auth Providers List -Medusa also defines a read-only link between the `LineItemAdjustment` and `Promotion` data models. This means you can retrieve the details of the promotion applied on a line item, but you don't manage the links in a pivot table in the database. The promotion of a line item is determined by the `promotion_id` property of the `LineItemAdjustment` data model. +- [Emailpass](https://docs.medusajs.com/commerce-modules/auth/auth-providers/emailpass/index.html.md) +- [Google](https://docs.medusajs.com/commerce-modules/auth/auth-providers/google/index.html.md) +- [GitHub](https://docs.medusajs.com/commerce-modules/auth/auth-providers/github/index.html.md) -### Retrieve with Query +*** -To retrieve the promotions of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotions.*` in `fields`: +## Configure Allowed Auth Providers of Actor Types -To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. +By default, users of all actor types can authenticate with all installed auth module providers. -### query.graph +To restrict the auth providers used for actor types, use the [authMethodsPerActor option](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthMethodsPerActor/index.html.md) in Medusa's configurations: -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "promotions.*", - ], +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + http: { + authMethodsPerActor: { + user: ["google"], + customer: ["emailpass"], + }, + // ... + }, + // ... + }, }) - -// carts.promotions ``` -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... +When you specify the `authMethodsPerActor` configuration, it overrides the default. So, if you don't specify any providers for an actor type, users of that actor type can't authenticate with any provider. -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "promotions.*", - ], -}) +*** -// carts.promotions -``` +## How to Create an Auth Module Provider -### Manage with Link +Refer to [this guide](https://docs.medusajs.com/references/auth/provider/index.html.md) to learn how to create an auth module provider. -To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -### link.create +# How to Create an Actor Type -```ts -import { Modules } from "@medusajs/framework/utils" +In this document, learn how to create an actor type and authenticate its associated data model. -// ... +## 0. Create Module with Data Model -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` +Before creating an actor type, you must have a module with a data model representing the actor type. -### createRemoteLinkStep +Learn how to create a module in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +The rest of this guide uses this `Manager` data model as an example: -// ... +```ts title="src/modules/manager/models/manager.ts" +import { model } from "@medusajs/framework/utils" -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, +const Manager = model.define("manager", { + id: model.id().primaryKey(), + firstName: model.text(), + lastName: model.text(), + email: model.text(), }) + +export default Manager ``` *** -## Region Module +## 1. Create Workflow -Medusa defines a read-only link between the `Cart` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of a cart's region, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. +Start by creating a workflow that does two things: -### Retrieve with Query +- Creates a record of the `Manager` data model. +- Sets the `app_metadata` property of the associated `AuthIdentity` record based on the new actor type. -To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: +For example, create the file `src/workflows/create-manager.ts`. with the following content: -### query.graph +```ts title="src/workflows/create-manager.ts" highlights={workflowHighlights} +import { + createWorkflow, + createStep, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + setAuthAppMetadataStep, +} from "@medusajs/medusa/core-flows" +import ManagerModuleService from "../modules/manager/service" -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "region.*", - ], -}) +type CreateManagerWorkflowInput = { + manager: { + first_name: string + last_name: string + email: string + } + authIdentityId: string +} -// carts.region -``` +const createManagerStep = createStep( + "create-manager-step", + async ({ + manager: managerData, + }: Pick, + { container }) => { + const managerModuleService: ManagerModuleService = + container.resolve("managerModuleService") -### useQueryGraphStep + const manager = await managerModuleService.createManager( + managerData + ) -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + return new StepResponse(manager) + } +) -// ... +const createManagerWorkflow = createWorkflow( + "create-manager", + function (input: CreateManagerWorkflowInput) { + const manager = createManagerStep({ + manager: input.manager, + }) -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "region.*", - ], -}) + setAuthAppMetadataStep({ + authIdentityId: input.authIdentityId, + actorType: "manager", + value: manager.id, + }) -// carts.region + return new WorkflowResponse(manager) + } +) + +export default createManagerWorkflow ``` +This workflow accepts the manager’s data and the associated auth identity’s ID as inputs. The next sections explain how the auth identity ID is retrieved. + +The workflow has two steps: + +1. Create the manager using the `createManagerStep`. +2. Set the `app_metadata` property of the associated auth identity using the `setAuthAppMetadataStep` from Medusa's core workflows. You specify the actor type `manager` in the `actorType` property of the step’s input. + *** -## Sales Channel Module +## 2. Define the Create API Route -Medusa defines a read-only link between the `Cart` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of a cart's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. +Next, you’ll use the workflow defined in the previous section in an API route that creates a manager. -### Retrieve with Query +So, create the file `src/api/manager/route.ts` with the following content: -To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: +```ts title="src/api/manager/route.ts" highlights={createRouteHighlights} +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { MedusaError } from "@medusajs/framework/utils" +import createManagerWorkflow from "../../workflows/create-manager" -### query.graph +type RequestBody = { + first_name: string + last_name: string + email: string +} -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) +export async function POST( + req: AuthenticatedMedusaRequest, + 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." + ) + } -// carts.sales_channel + const { result } = await createManagerWorkflow(req.scope) + .run({ + input: { + manager: req.body, + authIdentityId: req.auth_context.auth_identity_id, + }, + }) + + res.status(200).json({ manager: result }) +} ``` -### useQueryGraphStep +Since the manager must be associated with an `AuthIdentity` record, the request is expected to be authenticated, even if the manager isn’t created yet. This can be achieved by: -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +1. Obtaining a token usng the [/auth route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). +2. Passing the token in the bearer header of the request to this route. -// ... +In the API route, you create the manager using the workflow from the previous section and return it in the response. -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) +*** -// carts.sales_channel -``` +## 3. Apply the `authenticate` Middleware +The last step is to apply the `authenticate` middleware on the API routes that require a manager’s authentication. -# Promotions Adjustments in Carts +To do that, create the file `src/api/middlewares.ts` with the following content: -In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. +```ts title="src/api/middlewares.ts" highlights={middlewareHighlights} +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" -## What are Adjustment Lines? +export default defineMiddlewares({ + routes: [ + { + matcher: "/manager", + method: "POST", + middlewares: [ + authenticate("manager", ["session", "bearer"], { + allowUnregistered: true, + }), + ], + }, + { + matcher: "/manager/me*", + middlewares: [ + authenticate("manager", ["session", "bearer"]), + ], + }, + ], +}) +``` -An adjustment line indicates a change to an item or a shipping method’s amount. It’s used to apply promotions or discounts on a cart. +This applies middlewares on two route patterns: -The [LineItemAdjustment](https://docs.medusajs.com/references/cart/models/LineItemAdjustment/index.html.md) data model represents changes on a line item, and the [ShippingMethodAdjustment](https://docs.medusajs.com/references/cart/models/ShippingMethodAdjustment/index.html.md) data model represents changes on a shipping method. +1. The `authenticate` middleware is applied on the `/manager` API route for `POST` requests while allowing unregistered managers. This requires that a bearer token be passed in the request to access the manager’s auth identity but doesn’t require the manager to be registered. +2. The `authenticate` middleware is applied on all routes starting with `/manager/me`, restricting these routes to authenticated managers only. -![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg) +### Retrieve Manager API Route -The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. +For example, create the file `src/api/manager/me/route.ts` with the following content: -*** +```ts title="src/api/manager/me/route.ts" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import ManagerModuleService from "../../../modules/manager/service" -## Discountable Option +export async function GET( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +): Promise { + const managerModuleService: ManagerModuleService = + req.scope.resolve("managerModuleService") -The [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. + const manager = await managerModuleService.retrieveManager( + req.auth_context.actor_id + ) -When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules. + res.json({ manager }) +} +``` + +This route is only accessible by authenticated managers. You access the manager’s ID using `req.auth_context.actor_id`. *** -## Promotion Actions +## Test Custom Actor Type Authentication Flow -When using the Cart and Promotion modules together, such as in the Medusa application, use the [computeActions method of the Promotion Module’s main service](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). It retrieves the actions of line items and shipping methods. +To authenticate managers: -Learn more about actions in the [Promotion Module’s documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md). +1. Send a `POST` request to `/auth/manager/emailpass/register` to create an auth identity for the manager: -For example: +```bash +curl -X POST 'http://localhost:9000/auth/manager/emailpass/register' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "manager@gmail.com", + "password": "supersecret" +}' +``` -```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - ComputeActionAdjustmentLine, - ComputeActionItemLine, - ComputeActionShippingLine, - // ... -} from "@medusajs/framework/types" +Copy the returned token to use it in the next request. -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.adjustments", - "shipping_methods.adjustments", - ], -}) +2. Send a `POST` request to `/manager` to create a manager: -// 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, - }) - } -}) +```bash +curl -X POST 'http://localhost:9000/manager' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data-raw '{ + "first_name": "John", + "last_name": "Doe", + "email": "manager@gmail.com" +}' +``` -// 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, - }) - } -}) +Replace `{token}` with the token returned in the previous step. -// compute actions -const actions = await promotionModuleService.computeActions( - ["promo_123"], - { - items: lineItemAdjustments, - shipping_methods: shippingMethodAdjustments, - } -) +3. Send a `POST` request to `/auth/manager/emailpass` again to retrieve an authenticated token for the manager: + +```bash +curl -X POST 'http://localhost:9000/auth/manager/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "manager@gmail.com", + "password": "supersecret" +}' ``` -The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. +4. You can now send authenticated requests as a manager. For example, send a `GET` request to `/manager/me` to retrieve the authenticated manager’s details: -Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments. +```bash +curl 'http://localhost:9000/manager/me' \ +-H 'Authorization: Bearer {token}' +``` -```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - AddItemAdjustmentAction, - AddShippingMethodAdjustment, - // ... -} from "@medusajs/framework/types" +Whenever you want to log in as a manager, use the `/auth/manager/emailpass` API route, as explained in step 3. -// ... +*** -await cartModuleService.setLineItemAdjustments( - cart.id, - actions.filter( - (action) => action.action === "addItemAdjustment" - ) as AddItemAdjustmentAction[] -) +## Delete User of Actor Type -await cartModuleService.setShippingMethodAdjustments( - cart.id, - actions.filter( - (action) => - action.action === "addShippingMethodAdjustment" - ) as AddShippingMethodAdjustment[] -) -``` +When you delete a user of the actor type, you must update its auth identity to remove the association to the user. +For example, create the following workflow that deletes a manager and updates its auth identity, create the file `src/workflows/delete-manager.ts` with the following content: -# Tax Lines in Cart Module +```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import ManagerModuleService from "../modules/manager/service" -In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. +export type DeleteManagerWorkflow = { + id: string +} -## What are Tax Lines? +const deleteManagerStep = createStep( + "delete-manager-step", + async ( + { id }: DeleteManagerWorkflow, + { container }) => { + const managerModuleService: ManagerModuleService = + container.resolve("managerModuleService") -A tax line indicates the tax rate of a line item or a shipping method. The [LineItemTaxLine data model](https://docs.medusajs.com/references/cart/models/LineItemTaxLine/index.html.md) represents a line item’s tax line, and the [ShippingMethodTaxLine data model](https://docs.medusajs.com/references/cart/models/ShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. + const manager = await managerModuleService.retrieve(id) -![A diagram showcasing the relation between other data models and the tax line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534431/Medusa%20Resources/cart-tax-lines_oheaq6.jpg) + await managerModuleService.deleteManagers(id) -*** + return new StepResponse(undefined, { manager }) + }, + async ({ manager }, { container }) => { + const managerModuleService: ManagerModuleService = + container.resolve("managerModuleService") -## Tax Inclusivity + await managerModuleService.createManagers(manager) + } + ) +``` -By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount, and then adding them to the item/method’s subtotal. +You add a step that deletes the manager using the `deleteManagers` method of the module's main service. In the compensation function, you create the manager again. -However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. +Next, in the same file, add the workflow that deletes a manager: -So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. +```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={deleteHighlights} +// other imports +import { MedusaError } from "@medusajs/framework/utils" +import { + WorkflowData, + WorkflowResponse, + createWorkflow, + transform, +} from "@medusajs/framework/workflows-sdk" +import { + setAuthAppMetadataStep, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" -The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective. +// ... -![A diagram showing an example of calculating the subtotal of a line item using its taxes](https://res.cloudinary.com/dza7lstvk/image/upload/v1711535295/Medusa%20Resources/cart-tax-inclusive_shpr3t.jpg) +export const deleteManagerWorkflow = createWorkflow( + "delete-manager", + ( + input: WorkflowData + ): WorkflowResponse => { + deleteManagerStep(input) -For example, if a line item's amount is `5000`, the tax rate is `10`, and tax inclusivity is enabled, the tax amount is 10% of `5000`, which is `500`, making the unit price of the line item `4500`. + const { data: authIdentities } = useQueryGraphStep({ + entity: "auth_identity", + fields: ["id"], + filters: { + app_metadata: { + // the ID is of the format `{actor_type}_id`. + manager_id: input.id, + }, + }, + }) -*** + const authIdentity = transform( + { authIdentities }, + ({ authIdentities }) => { + const authIdentity = authIdentities[0] -## Retrieve Tax Lines + if (!authIdentity) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + "Auth identity not found" + ) + } -When using the Cart and Tax modules together, you can use the `getTaxLines` method of the Tax Module’s main service. It retrieves the tax lines for a cart’s line items and shipping methods. + return authIdentity + } + ) -```ts -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.tax_lines", - "shipping_methods.tax_lines", - "shipping_address", - ], -}) + setAuthAppMetadataStep({ + authIdentityId: authIdentity.id, + actorType: "manager", + value: null, + }) -// 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", - }, + return new WorkflowResponse(input.id) } ) ``` -Then, use the returned tax lines to set the line items and shipping methods’ tax lines: +In the workflow, you: -```ts -// set line item tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "line_item_id" in line) -) +1. Use the `deleteManagerStep` defined earlier to delete the manager. +2. Retrieve the auth identity of the manager using Query. To do that, you filter the `app_metadata` property of an auth identity, which holds the user's ID under `{actor_type_name}_id`. So, in this case, it's `manager_id`. +3. Check that the auth identity exist, then, update the auth identity to remove the ID of the manager from it. -// set shipping method tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "shipping_line_id" in line) -) -``` +You can use this workflow when deleting a manager, such as in an API route. -# Customer Accounts +# How to Use Authentication Routes -In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. +In this document, you'll learn about the authentication routes and how to use them to create and log-in users, and reset their password. -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard. +These routes are added by Medusa's HTTP layer, not the Auth Module. -## `has_account` Property +## Types of Authentication Flows -The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered. +### 1. Basic Authentication Flow -When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. +This authentication flow doesn't require validation with third-party services. -When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`. +[How to register customer in storefront using basic authentication flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md). -*** +The steps are: -## Email Uniqueness +![Diagram showcasing the basic authentication flow between the frontend and the Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1725539370/Medusa%20Resources/basic-auth-routes_pgpjch.jpg) -The above behavior means that two `Customer` records may exist with the same email. However, the main difference is the `has_account` property's value. +1. Register the user with the [Register Route](#register-route). +2. Use the authentication token to create the user with their respective API route. + - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers). + - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept) +3. Authenticate the user with the [Auth Route](#login-route). -So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. +After registration, you only use the [Auth Route](#login-route) for subsequent authentication. +To handle errors related to existing identities, refer to [this section](#handling-existing-identities). -# Links between Customer Module and Other Modules +### 2. Third-Party Service Authenticate Flow -This document showcases the module links defined between the Customer Module and other commerce modules. +This authentication flow authenticates the user with a third-party service, such as Google. -## Summary +[How to authenticate customer with a third-party provider in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). -The Customer Module has the following links to other modules: +It requires the following steps: -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +![Diagram showcasing the authentication flow between the frontend, Medusa application, and third-party service](https://res.cloudinary.com/dza7lstvk/image/upload/v1725528159/Medusa%20Resources/Third_Party_Auth_tvf4ng.jpg) -- [`Customer` data model \<> `AccountHolder` data model of Payment Module](#payment-module). -- [`Cart` data model of Cart Module \<> `Customer` data model](#cart-module). (Read-only). -- [`Order` data model of Order Module \<> `Customer` data model](#order-module). (Read-only). +1. Authenticate the user with the [Auth Route](#login-route). +2. The auth route returns a URL to authenticate with third-party service, such as login with Google. The frontend (such as a storefront), when it receives a `location` property in the response, must redirect to the returned location. +3. Once the authentication with the third-party service finishes, it redirects back to the frontend with a `code` query parameter. So, make sure your third-party service is configured to redirect to your frontend page after successful authentication. +4. The frontend sends a request to the [Validate Callback Route](#validate-callback-route) passing it the query parameters received from the third-party service, such as the `code` and `state` query parameters. +5. If the callback validation is successful, the frontend receives the authentication token. +6. Decode the received token in the frontend using tools like [react-jwt](https://www.npmjs.com/package/react-jwt). + - If the decoded data has an `actor_id` property, then the user is already registered. So, use this token for subsequent authenticated requests. + - If not, follow the rest of the steps. +7. The frontend uses the authentication token to create the user with their respective API route. + - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers). + - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept) +8. The frontend sends a request to the [Refresh Token Route](#refresh-token-route) to retrieve a new token with the user information populated. *** -## Payment Module - -Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it. - -This link is available starting from Medusa `v2.5.0`. +## Register Route -### Retrieve with Query +The Medusa application defines an API route at `/auth/{actor_type}/{provider}/register` that creates an auth identity for an actor type, such as a `customer`. It returns a JWT token that you pass to an API route that creates the user. -To retrieve the account holder associated with a customer with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "Whitney_Schultz@gmail.com" + // ... +}' +``` -### query.graph +This API route is useful for providers like `emailpass` that uses custom logic to authenticate a user. For authentication providers that authenticate with third-party services, such as Google, use the [Auth Route](#login-route) instead. -```ts -const { data: customers } = await query.graph({ - entity: "customer", - fields: [ - "account_holder.*", - ], -}) +For example, if you're registering a customer, you: -// customers.account_holder -``` +1. Send a request to `/auth/customer/emailpass/register` to retrieve the registration JWT token. +2. Send a request to the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers) to create the customer, passing the [JWT token in the header](https://docs.medusajs.com/api/store#authentication). -### useQueryGraphStep +### Path Parameters -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +Its path parameters are: -// ... +- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. +- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. -const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: [ - "account_holder.*", - ], -}) +### Request Body Parameters -// customers.account_holder -``` +This route accepts in the request body the data that the specified authentication provider requires to handle authentication. -### Manage with Link +For example, the EmailPass provider requires an `email` and `password` fields in the request body. -To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +### Response Fields -### link.create +If the authentication is successful, you'll receive a `token` field in the response body object: -```ts -import { Modules } from "@medusajs/framework/utils" +```json +{ + "token": "..." +} +``` -// ... +Use that token in the header of subsequent requests to send authenticated requests. -await link.create({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, -}) -``` +### Handling Existing Identities -### createRemoteLinkStep +An auth identity with the same email may already exist in Medusa. This can happen if: -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +- Another actor type is using that email. For example, an admin user is trying to register as a customer. +- The same email belongs to a record of the same actor type. For example, another customer has the same email. -// ... +In these scenarios, the Register Route will return an error instead of a token: -createRemoteLinkStep({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, -}) +```json +{ + "type": "unauthorized", + "message": "Identity with email already exists" +} ``` -*** +To handle these scenarios, you can use the [Login Route](#login-route) to validate that the email and password match the existing identity. If so, you can allow the admin user, for example, to register as a customer. -## Cart Module +Otherwise, if the email and password don't match the existing identity, such as when the email belongs to another customer, the [Login Route](#login-route) returns an error: -Medusa defines a read-only link between the `Customer` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model. This means you can retrieve the details of a customer's carts, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model. +```json +{ + "type": "unauthorized", + "message": "Invalid email or password" +} +``` -### Retrieve with Query +You can show that error message to the customer. -To retrieve a customer's carts with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: +*** -### query.graph +## Login Route -```ts -const { data: customers } = await query.graph({ - entity: "customer", - fields: [ - "carts.*", - ], -}) +The Medusa application defines an API route at `/auth/{actor_type}/{provider}` that authenticates a user of an actor type. It returns a JWT token that can be passed in [the header of subsequent requests](https://docs.medusajs.com/api/store#authentication) to send authenticated requests. -// customers.carts +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers} +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "Whitney_Schultz@gmail.com" + // ... +}' ``` -### useQueryGraphStep +For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +### Path Parameters -// ... +Its path parameters are: -const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: [ - "carts.*", - ], -}) +- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. +- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. -// customers.carts -``` +### Request Body Parameters -*** +This route accepts in the request body the data that the specified authentication provider requires to handle authentication. -## Order Module +For example, the EmailPass provider requires an `email` and `password` fields in the request body. -Medusa defines a read-only link between the `Customer` data model and the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model. This means you can retrieve the details of a customer's orders, but you don't manage the links in a pivot table in the database. The customer of an order is determined by the `customer_id` property of the `Order` data model. +#### Overriding Callback URL -### Retrieve with Query +For the [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md) and [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) providers, you can pass a `callback_url` body parameter that overrides the `callbackUrl` set in the provider's configurations. -To retrieve a customer's orders with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: +This is useful if you want to redirect the user to a different URL after authentication based on their actor type. For example, you can set different `callback_url` for admin users and customers. -### query.graph +### Response Fields -```ts -const { data: customers } = await query.graph({ - entity: "customer", - fields: [ - "orders.*", - ], -}) +If the authentication is successful, you'll receive a `token` field in the response body object: -// customers.orders +```json +{ + "token": "..." +} ``` -### useQueryGraphStep +Use that token in the header of subsequent requests to send authenticated requests. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +If the authentication requires more action with a third-party service, you'll receive a `location` property: -// ... +```json +{ + "location": "https://..." +} +``` -const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: [ - "orders.*", - ], -}) +Redirect to that URL in the frontend to continue the authentication process with the third-party service. -// customers.orders -``` +[How to login Customers using the authentication route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/login/index.html.md). +*** -# Authentication Flows with the Auth Main Service +## Validate Callback Route -In this document, you'll learn how to use the Auth Module's main service's methods to implement authentication flows and reset a user's password. +The Medusa application defines an API route at `/auth/{actor_type}/{provider}/callback` that's useful for validating the authentication callback or redirect from third-party services like Google. -## Authentication Methods +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/callback?code=123&state=456 +``` -### Register +Refer to the [third-party authentication flow](#2-third-party-service-authenticate-flow) section to see how this route fits into the authentication flow. -The [register method of the Auth Module's main service](https://docs.medusajs.com/references/auth/register/index.html.md) creates an auth identity that can be authenticated later. +### Path Parameters -For example: +Its path parameters are: -```ts -const data = await authModuleService.register( - "emailpass", - // passed to auth provider - { - // ... - } -) -``` +- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. +- `{provider}`: the auth provider to handle the authentication. For example, `google`. -This method calls the `register` method of the provider specified in the first parameter and returns its data. +### Query Parameters -### Authenticate +This route accepts all the query parameters that the third-party service sends to the frontend after the user completes the authentication process, such as the `code` and `state` query parameters. -To authenticate a user, you use the [authenticate method of the Auth Module's main service](https://docs.medusajs.com/references/auth/authenticate/index.html.md). For example: +### Response Fields -```ts -const data = await authModuleService.authenticate( - "emailpass", - // passed to auth provider - { - // ... - } -) +If the authentication is successful, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." +} ``` -This method calls the `authenticate` method of the provider specified in the first parameter and returns its data. +In your frontend, decode the token using tools like [react-jwt](https://www.npmjs.com/package/react-jwt): + +- If the decoded data has an `actor_id` property, the user is already registered. So, use this token for subsequent authenticated requests. +- If not, use the token in the header of a request that creates the user, such as the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers). *** -## Auth Flow 1: Basic Authentication +## Refresh Token Route -The basic authentication flow requires first using the `register` method, then the `authenticate` method: +The Medusa application defines an API route at `/auth/token/refresh` that's useful after authenticating a user with a third-party service to populate the user's token with their new information. -```ts -const { success, authIdentity, error } = await authModuleService.register( - "emailpass", - // passed to auth provider - { - // ... - } -) +It requires the user's JWT token that they received from the authentication or callback routes. -if (error) { - // registration failed - // TODO return an error - return -} +```bash +curl -X POST http://localhost:9000/auth/token/refresh \ +-H 'Authorization: Bearer {token}' +``` -// later (can be another route for log-in) -const { success, authIdentity, location } = await authModuleService.authenticate( - "emailpass", - // passed to auth provider - { - // ... - } -) +### Response Fields -if (success && !location) { - // user is authenticated +If the token was refreshed successfully, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." } ``` -If `success` is true and `location` isn't set, the user is authenticated successfully, and their authentication details are available within the `authIdentity` object. +Use that token in the header of subsequent requests to send authenticated requests. -The next section explains the flow if `location` is set. +*** -Check out the [AuthIdentity](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) reference for the received properties in `authIdentity`. +## Reset Password Routes -![Diagram showcasing the basic authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711373749/Medusa%20Resources/basic-auth_lgpqsj.jpg) +To reset a user's password: -### Auth Identity with Same Identifier +1. Generate a token using the [Generate Reset Password Token API route](#generate-reset-password-token-route). + - The API route emits the `auth.password_reset` event, passing the token in the payload. + - You can create a subscriber, as seen in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/reset-password/index.html.md), that listens to the event and send a notification to the user. +2. Pass the token to the [Reset Password API route](#reset-password-route) to reset the password. + - The URL in the user's notification should direct them to a frontend URL, which sends a request to this route. -If an auth identity, such as a `customer`, tries to register with an email of another auth identity, the `register` method returns an error. This can happen either if another customer is using the same email, or an admin user has the same email. +[Storefront Development: How to Reset a Customer's Password.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) -There are two ways to handle this: +### Generate Reset Password Token Route -- Consider the customer authenticated if the `authenticate` method validates that the email and password are correct. This allows admin users, for example, to authenticate as customers. -- Return an error message to the customer, informing them that the email is already in use. +The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/reset-password` that emits the `auth.password_reset` event, passing the token in the payload. -*** +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/reset-password +-H 'Content-Type: application/json' \ +--data-raw '{ + "identifier": "Whitney_Schultz@gmail.com" +}' +``` -## Auth Flow 2: Third-Party Service Authentication +This API route is useful for providers like `emailpass` that store a user's password and use it for authentication. -The third-party service authentication method requires using the `authenticate` method first: +#### Path Parameters -```ts -const { success, authIdentity, location } = await authModuleService.authenticate( - "google", - // passed to auth provider - { - // ... - } -) +Its path parameters are: -if (location) { - // return the location for the front-end to redirect to -} +- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. +- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. -if (!success) { - // authentication failed -} +#### Request Body Parameters -// authentication successful -``` +This route accepts in the request body an object having the following property: -If the `authenticate` method returns a `location` property, the authentication process requires the user to perform an action with a third-party service. So, you return the `location` to the front-end or client to redirect to that URL. +- `identifier`: The user's identifier in the specified auth provider. For example, for the `emailpass` auth provider, you pass the user's email. -For example, when using the `google` provider, the `location` is the URL that the user is navigated to login. +#### Response Fields -![Diagram showcasing the first part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711374847/Medusa%20Resources/third-party-auth-1_enyedy.jpg) +If the authentication is successful, the request returns a `201` response code. -### Overriding Callback URL +### Reset Password Route -The Google and GitHub providers allow you to override their `callbackUrl` option during authentication. This is useful when you redirect the user after authentication to a URL based on its actor type. For example, you redirect admin users and customers to different pages. +The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/update` that accepts a token and, if valid, updates the user's password. -```ts -const { success, authIdentity, location } = await authModuleService.authenticate( - "google", - // passed to auth provider - { - // ... - callback_url: "example.com", - } -) +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/update +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data-raw '{ + "email": "Whitney_Schultz@gmail.com", + "password": "supersecret" +}' ``` -### validateCallback - -Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service. +This API route is useful for providers like `emailpass` that store a user's password and use it for logging them in. -So, once the user performs the required action with the third-party service (for example, log-in with Google), the frontend must redirect to an API route that uses the [validateCallback method of the Auth Module's main service](https://docs.medusajs.com/references/auth/validateCallback/index.html.md). +#### Path Parameters -The method calls the specified provider’s `validateCallback` method passing it the authentication details it received in the second parameter: +Its path parameters are: -```ts -const { success, authIdentity } = await authModuleService.validateCallback( - "google", - // passed to auth provider - { - // request data, such as - url, - headers, - query, - body, - protocol, - } -) +- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. +- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. -if (success) { - // authentication succeeded -} -``` +#### Pass Token in Authorization Header -For providers like Google, the `query` object contains the query parameters from the original callback URL, such as the `code` and `state` parameters. +Before [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6), you passed the token as a query parameter. Now, you must pass it in the `Authorization` header. -If the returned `success` property is `true`, the authentication with the third-party provider was successful. +In the request's authorization header, you must pass the token generated using the [Generate Reset Password Token route](#generate-reset-password-token-route). You pass it as a bearer token. -![Diagram showcasing the second part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711375123/Medusa%20Resources/third-party-auth-2_kmjxju.jpg) +### Request Body Parameters -*** +This route accepts in the request body an object that has the data necessary for the provider to update the user's password. -## Reset Password +For the `emailpass` provider, you must pass the following properties: -To update a user's password or other authentication details, use the `updateProvider` method of the Auth Module's main service. It calls the `update` method of the specified authentication provider. +- `email`: The user's email. +- `password`: The new password. -For example: +### Response Fields -```ts -const { success } = await authModuleService.updateProvider( - "emailpass", - // passed to the auth provider - { - entity_id: "user@example.com", - password: "supersecret", - } -) +If the authentication is successful, the request returns an object with a `success` property set to `true`: -if (success) { - // password reset successfully +```json +{ + "success": "true" } ``` -The method accepts as a first parameter the ID of the provider, and as a second parameter the data necessary to reset the password. - -In the example above, you use the `emailpass` provider, so you have to pass an object having an `email` and `password` properties. - -If the returned `success` property is `true`, the password has reset successfully. - - -# Auth Providers - -In this document, you’ll learn how the Auth Module handles authentication using providers. -## What's an Auth Module Provider? +# Auth Module Options -An auth module provider handles authenticating customers and users, either using custom logic or by integrating a third-party service. +In this document, you'll learn about the options of the Auth Module. -For example, the EmailPass Auth Module Provider authenticates a user using their email and password, whereas the Google Auth Module Provider authenticates users using their Google account. +## providers -### Auth Providers List +The `providers` option is an array of auth module providers. -- [Emailpass](https://docs.medusajs.com/commerce-modules/auth/auth-providers/emailpass/index.html.md) -- [Google](https://docs.medusajs.com/commerce-modules/auth/auth-providers/google/index.html.md) -- [GitHub](https://docs.medusajs.com/commerce-modules/auth/auth-providers/github/index.html.md) +When the Medusa application starts, these providers are registered and can be used to handle authentication. -*** +By default, the `emailpass` provider is registered to authenticate customers and admin users. -## Configure Allowed Auth Providers of Actor Types +For example: -By default, users of all actor types can authenticate with all installed auth module providers. +```ts title="medusa-config.ts" +import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" -To restrict the auth providers used for actor types, use the [authMethodsPerActor option](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthMethodsPerActor/index.html.md) in Medusa's configurations: +// ... -```ts title="medusa-config.ts" module.exports = defineConfig({ - projectConfig: { - http: { - authMethodsPerActor: { - user: ["google"], - customer: ["emailpass"], + // ... + modules: [ + { + resolve: "@medusajs/medusa/auth", + dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], + options: { + providers: [ + { + resolve: "@medusajs/medusa/auth-emailpass", + id: "emailpass", + options: { + // provider options... + }, + }, + ], }, - // ... }, - // ... - }, + ], }) ``` -When you specify the `authMethodsPerActor` configuration, it overrides the default. So, if you don't specify any providers for an actor type, users of that actor type can't authenticate with any provider. +The `providers` option is an array of objects that accept the following properties: + +- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory. +- `id`: A string indicating the provider's unique name or ID. +- `options`: An optional object of the module provider's options. *** -## How to Create an Auth Module Provider +## Auth CORS -Refer to [this guide](https://docs.medusajs.com/references/auth/provider/index.html.md) to learn how to create an auth module provider. +The Medusa application's authentication API routes are defined under the `/auth` prefix that requires setting the `authCors` property of the `http` configuration. +By default, the Medusa application you created will have an `AUTH_CORS` environment variable, which is used as the value of `authCors`. -# Auth Identity and Actor Types +Refer to [Medusa's configuration guide](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthCors/index.html.md) to learn more about the `authCors` configuration. -In this document, you’ll learn about concepts related to identity and actors in the Auth Module. +*** -## What is an Auth Identity? +## authMethodsPerActor Configuration -The [AuthIdentity data model](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) represents a user registered by an [authentication provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). When a user is registered using an authentication provider, the provider creates a record of `AuthIdentity`. +The Medusa application's configuration accept an `authMethodsPerActor` configuration which restricts the allowed auth providers used with an actor type. -Then, when the user logs-in in the future with the same authentication provider, the associated auth identity is used to validate their credentials. +Learn more about the `authMethodsPerActor` configuration in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers#configure-allowed-auth-providers-of-actor-types/index.html.md). -*** -## Actor Types +# How to Handle Password Reset Token Event -An actor type is a type of user that can be authenticated. The Auth Module doesn't store or manage any user-like models, such as for customers or users. Instead, the user types are created and managed by other modules. For example, a customer is managed by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md). +In this guide, you'll learn how to handle the `auth.password_reset` event, which is emitted when a request is sent to the [Generate Reset Password Token API route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#generate-reset-password-token-route/index.html.md). -Then, when an auth identity is created for the actor type, the ID of the user is stored in the `app_metadata` property of the auth identity. +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/reset-password/index.html.md) to learn how to reset your user admin password using the dashboard. -For example, an auth identity of a customer has the following `app_metadata` property: +You'll create a subscriber that listens to the event. When the event is emitted, the subscriber sends an email notification to the user. -```json -{ - "app_metadata": { - "customer_id": "cus_123" - } -} -``` +### Prerequisites -The ID of the user is stored in the key `{actor_type}_id` of the `app_metadata` property. +- [A notification provider module, such as SendGrid](https://docs.medusajs.com/architectural-modules/notification/sendgrid/index.html.md) -*** +## 1. Create Subscriber -## Protect Routes by Actor Type +The first step is to create a subscriber that listens to the `auth.password_reset` and sends the user a notification with instructions to reset their password. -When you protect routes with the `authenticate` middleware, you specify in its first parameter the actor type that must be authenticated to access the specified API routes. +Create the file `src/subscribers/handle-reset.ts` with the following content: -For example: +```ts title="src/subscribers/handle-reset.ts" highlights={highlights} collapsibleLines="1-6" expandMoreLabel="Show Imports" +import { + SubscriberArgs, + type SubscriberConfig, +} from "@medusajs/medusa" +import { Modules } from "@medusajs/framework/utils" -```ts title="src/api/middlewares.ts" highlights={highlights} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" +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 + ) -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom/admin*", - middlewares: [ - authenticate("user", ["session", "bearer", "api-key"]), - ], + const urlPrefix = actor_type === "customer" ? + "https://storefront.com" : + "https://admin.com/app" + + 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}`, }, - ], -}) -``` + }) +} -By specifying `user` as the first parameter of `authenticate`, only authenticated users of actor type `user` (admin users) can access API routes starting with `/custom/admin`. +export const config: SubscriberConfig = { + event: "auth.password_reset", +} +``` -*** +You subscribe to the `auth.password_reset` event. The event has a data payload object with the following properties: -## Custom Actor Types - -You can define custom actor types that allows a custom user, managed by your custom module, to authenticate into Medusa. +- `entity_id`: The identifier of the user. When using the `emailpass` provider, it's the user's email. +- `token`: The token to reset the user's password. +- `actor_type`: The user's actor type. For example, if the user is a customer, the `actor_type` is `customer`. If it's an admin user, the `actor_type` is `user`. -For example, if you have a custom module with a `Manager` data model, you can authenticate managers with the `manager` actor type. +This event's payload previously had an `actorType` field. It was renamed to `actor_type` after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). -Learn how to create a custom actor type in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md). +In the subscriber, you: +- Decide the frontend URL based on whether the user is a customer or admin user by checking the value of `actor_type`. +- Resolve the Notification Module and use its `createNotifications` method to send the notification. +- You pass to the `createNotifications` method an object having the following properties: + - `to`: The identifier to send the notification to, which in this case is the email. + - `channel`: The channel to send the notification through, which in this case is email. + - `template`: The template ID in the third-party service. + - `data`: The data payload to pass to the template. You pass the URL to redirect the user to. You must pass the token and email in the URL so that the frontend can send them later to the Medusa application when reseting the password. -# How to Use Authentication Routes +*** -In this document, you'll learn about the authentication routes and how to use them to create and log-in users, and reset their password. +## 2. Test it Out: Generate Reset Password Token -These routes are added by Medusa's HTTP layer, not the Auth Module. +To test the subscriber out, send a request to the `/auth/{actor_type}/{auth_provider}/reset-password` API route, replacing `{actor_type}` and `{auth_provider}` with the user's actor type and provider used for authentication respectively. -## Types of Authentication Flows +For example, to generate a reset password token for an admin user using the `emailpass` provider, send the following request: -### 1. Basic Authentication Flow +```bash +curl --location 'http://localhost:9000/auth/user/emailpass/reset-password' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "identifier": "admin-test@gmail.com" +}' +``` -This authentication flow doesn't require validation with third-party services. +In the request body, you must pass an `identifier` parameter. Its value is the user's identifier, which is the email in this case. -[How to register customer in storefront using basic authentication flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md). +If the token is generated successfully, the request returns a response with `201` status code. In the terminal, you'll find the following message indicating that the `auth.password_reset` event was emitted and your subscriber ran: -The steps are: +```plain +info: Processing auth.password_reset which has 1 subscribers +``` -![Diagram showcasing the basic authentication flow between the frontend and the Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1725539370/Medusa%20Resources/basic-auth-routes_pgpjch.jpg) +The notification is sent to the user with the frontend URL to enter a new password. -1. Register the user with the [Register Route](#register-route). -2. Use the authentication token to create the user with their respective API route. - - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers). - - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept) -3. Authenticate the user with the [Auth Route](#login-route). +*** -After registration, you only use the [Auth Route](#login-route) for subsequent authentication. +## Next Steps: Implementing Frontend -To handle errors related to existing identities, refer to [this section](#handling-existing-identities). +In your frontend, you must have a page that accepts `token` and `email` query parameters. -### 2. Third-Party Service Authenticate Flow +The page shows the user password fields to enter their new password, then submits the new password, token, and email to the [Reset Password Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#reset-password-route/index.html.md). -This authentication flow authenticates the user with a third-party service, such as Google. +### Examples -[How to authenticate customer with a third-party provider in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). +- [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) -It requires the following steps: -![Diagram showcasing the authentication flow between the frontend, Medusa application, and third-party service](https://res.cloudinary.com/dza7lstvk/image/upload/v1725528159/Medusa%20Resources/Third_Party_Auth_tvf4ng.jpg) +# Links between Currency Module and Other Modules -1. Authenticate the user with the [Auth Route](#login-route). -2. The auth route returns a URL to authenticate with third-party service, such as login with Google. The frontend (such as a storefront), when it receives a `location` property in the response, must redirect to the returned location. -3. Once the authentication with the third-party service finishes, it redirects back to the frontend with a `code` query parameter. So, make sure your third-party service is configured to redirect to your frontend page after successful authentication. -4. The frontend sends a request to the [Validate Callback Route](#validate-callback-route) passing it the query parameters received from the third-party service, such as the `code` and `state` query parameters. -5. If the callback validation is successful, the frontend receives the authentication token. -6. Decode the received token in the frontend using tools like [react-jwt](https://www.npmjs.com/package/react-jwt). - - If the decoded data has an `actor_id` property, then the user is already registered. So, use this token for subsequent authenticated requests. - - If not, follow the rest of the steps. -7. The frontend uses the authentication token to create the user with their respective API route. - - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers). - - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept) -8. The frontend sends a request to the [Refresh Token Route](#refresh-token-route) to retrieve a new token with the user information populated. +This document showcases the module links defined between the Currency Module and other commerce modules. -*** +## Summary -## Register Route +The Currency Module has the following links to other modules: -The Medusa application defines an API route at `/auth/{actor_type}/{provider}/register` that creates an auth identity for an actor type, such as a `customer`. It returns a JWT token that you pass to an API route that creates the user. +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com" - // ... -}' -``` +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Read-only|| -This API route is useful for providers like `emailpass` that uses custom logic to authenticate a user. For authentication providers that authenticate with third-party services, such as Google, use the [Auth Route](#login-route) instead. +*** -For example, if you're registering a customer, you: +## Store Module -1. Send a request to `/auth/customer/emailpass/register` to retrieve the registration JWT token. -2. Send a request to the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers) to create the customer, passing the [JWT token in the header](https://docs.medusajs.com/api/store#authentication). +The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. -### Path Parameters +Instead, Medusa defines a read-only link between the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `Currency` data model and the Currency Module's `Currency` data model. Because the link is read-only from the `Store`'s side, you can only retrieve the details of a store's supported currencies, and not the other way around. -Its path parameters are: +### Retrieve with Query -- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. -- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. +To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`: -### Request Body Parameters +### query.graph -This route accepts in the request body the data that the specified authentication provider requires to handle authentication. +```ts +const { data: stores } = await query.graph({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) -For example, the EmailPass provider requires an `email` and `password` fields in the request body. +// stores.supported_currencies +``` -### Response Fields +### useQueryGraphStep -If the authentication is successful, you'll receive a `token` field in the response body object: +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -```json -{ - "token": "..." -} -``` +// ... -Use that token in the header of subsequent requests to send authenticated requests. +const { data: stores } = useQueryGraphStep({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) -### Handling Existing Identities +// stores.supported_currencies +``` -An auth identity with the same email may already exist in Medusa. This can happen if: -- Another actor type is using that email. For example, an admin user is trying to register as a customer. -- The same email belongs to a record of the same actor type. For example, another customer has the same email. +# Customer Accounts -In these scenarios, the Register Route will return an error instead of a token: +In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. -```json -{ - "type": "unauthorized", - "message": "Identity with email already exists" -} -``` +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard. -To handle these scenarios, you can use the [Login Route](#login-route) to validate that the email and password match the existing identity. If so, you can allow the admin user, for example, to register as a customer. +## `has_account` Property -Otherwise, if the email and password don't match the existing identity, such as when the email belongs to another customer, the [Login Route](#login-route) returns an error: +The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered. -```json -{ - "type": "unauthorized", - "message": "Invalid email or password" -} -``` +When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. -You can show that error message to the customer. +When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`. *** -## Login Route +## Email Uniqueness -The Medusa application defines an API route at `/auth/{actor_type}/{provider}` that authenticates a user of an actor type. It returns a JWT token that can be passed in [the header of subsequent requests](https://docs.medusajs.com/api/store#authentication) to send authenticated requests. +The above behavior means that two `Customer` records may exist with the same email. However, the main difference is the `has_account` property's value. -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers} --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com" - // ... -}' -``` +So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. -For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`. -### Path Parameters +# Links between Customer Module and Other Modules -Its path parameters are: +This document showcases the module links defined between the Customer Module and other commerce modules. -- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. -- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. +## Summary -### Request Body Parameters +The Customer Module has the following links to other modules: -This route accepts in the request body the data that the specified authentication provider requires to handle authentication. +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -For example, the EmailPass provider requires an `email` and `password` fields in the request body. +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|| in |Stored|| +| in ||Read-only|| +| in ||Read-only|| -#### Overriding Callback URL +*** -For the [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md) and [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) providers, you can pass a `callback_url` body parameter that overrides the `callbackUrl` set in the provider's configurations. +## Payment Module -This is useful if you want to redirect the user to a different URL after authentication based on their actor type. For example, you can set different `callback_url` for admin users and customers. +Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it. -### Response Fields +This link is available starting from Medusa `v2.5.0`. -If the authentication is successful, you'll receive a `token` field in the response body object: +### Retrieve with Query -```json -{ - "token": "..." -} -``` +To retrieve the account holder associated with a customer with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: -Use that token in the header of subsequent requests to send authenticated requests. +### query.graph -If the authentication requires more action with a third-party service, you'll receive a `location` property: +```ts +const { data: customers } = await query.graph({ + entity: "customer", + fields: [ + "account_holder.*", + ], +}) -```json -{ - "location": "https://..." -} +// customers.account_holder ``` -Redirect to that URL in the frontend to continue the authentication process with the third-party service. - -[How to login Customers using the authentication route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/login/index.html.md). +### useQueryGraphStep -*** +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -## Validate Callback Route +// ... -The Medusa application defines an API route at `/auth/{actor_type}/{provider}/callback` that's useful for validating the authentication callback or redirect from third-party services like Google. +const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: [ + "account_holder.*", + ], +}) -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/callback?code=123&state=456 +// customers.account_holder ``` -Refer to the [third-party authentication flow](#2-third-party-service-authenticate-flow) section to see how this route fits into the authentication flow. - -### Path Parameters - -Its path parameters are: - -- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. -- `{provider}`: the auth provider to handle the authentication. For example, `google`. +### Manage with Link -### Query Parameters +To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -This route accepts all the query parameters that the third-party service sends to the frontend after the user completes the authentication process, such as the `code` and `state` query parameters. +### link.create -### Response Fields +```ts +import { Modules } from "@medusajs/framework/utils" -If the authentication is successful, you'll receive a `token` field in the response body object: +// ... -```json -{ - "token": "..." -} +await link.create({ + [Modules.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", + }, +}) ``` -In your frontend, decode the token using tools like [react-jwt](https://www.npmjs.com/package/react-jwt): - -- If the decoded data has an `actor_id` property, the user is already registered. So, use this token for subsequent authenticated requests. -- If not, use the token in the header of a request that creates the user, such as the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers). - -*** - -## Refresh Token Route +### createRemoteLinkStep -The Medusa application defines an API route at `/auth/token/refresh` that's useful after authenticating a user with a third-party service to populate the user's token with their new information. +```ts +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -It requires the user's JWT token that they received from the authentication or callback routes. +// ... -```bash -curl -X POST http://localhost:9000/auth/token/refresh \ --H 'Authorization: Bearer {token}' +createRemoteLinkStep({ + [Modules.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", + }, +}) ``` -### Response Fields +*** -If the token was refreshed successfully, you'll receive a `token` field in the response body object: +## Cart Module -```json -{ - "token": "..." -} -``` +Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Customer` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the customer of a cart, and not the other way around. -Use that token in the header of subsequent requests to send authenticated requests. +### Retrieve with Query -*** +To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: -## Reset Password Routes +### query.graph -To reset a user's password: +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "customer.*", + ], +}) -1. Generate a token using the [Generate Reset Password Token API route](#generate-reset-password-token-route). - - The API route emits the `auth.password_reset` event, passing the token in the payload. - - You can create a subscriber, as seen in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/reset-password/index.html.md), that listens to the event and send a notification to the user. -2. Pass the token to the [Reset Password API route](#reset-password-route) to reset the password. - - The URL in the user's notification should direct them to a frontend URL, which sends a request to this route. +// carts.customer +``` -[Storefront Development: How to Reset a Customer's Password.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) +### useQueryGraphStep -### Generate Reset Password Token Route +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/reset-password` that emits the `auth.password_reset` event, passing the token in the payload. +// ... -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/reset-password --H 'Content-Type: application/json' \ ---data-raw '{ - "identifier": "Whitney_Schultz@gmail.com" -}' +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "customer.*", + ], +}) + +// carts.customer ``` -This API route is useful for providers like `emailpass` that store a user's password and use it for authentication. +*** -#### Path Parameters +## Order Module -Its path parameters are: +Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Customer` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the customer of an order, and not the other way around. -- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. -- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. +### Retrieve with Query -#### Request Body Parameters +To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: -This route accepts in the request body an object having the following property: +### query.graph -- `identifier`: The user's identifier in the specified auth provider. For example, for the `emailpass` auth provider, you pass the user's email. +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "customer.*", + ], +}) -#### Response Fields +// orders.customer +``` -If the authentication is successful, the request returns a `201` response code. +### useQueryGraphStep -### Reset Password Route +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/update` that accepts a token and, if valid, updates the user's password. +// ... -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/update --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com", - "password": "supersecret" -}' -``` +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "customer.*", + ], +}) -This API route is useful for providers like `emailpass` that store a user's password and use it for logging them in. +// orders.customer +``` -#### Path Parameters -Its path parameters are: +# Cart Concepts -- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. -- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. +In this document, you’ll get an overview of the main concepts of a cart. -#### Pass Token in Authorization Header +## Shipping and Billing Addresses -Before [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6), you passed the token as a query parameter. Now, you must pass it in the `Authorization` header. +A cart has a shipping and billing address. Both of these addresses are represented by the [Address data model](https://docs.medusajs.com/references/cart/models/Address/index.html.md). -In the request's authorization header, you must pass the token generated using the [Generate Reset Password Token route](#generate-reset-password-token-route). You pass it as a bearer token. +![A diagram showcasing the relation between the Cart and Address data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711532392/Medusa%20Resources/cart-addresses_ls6qmv.jpg) -### Request Body Parameters +*** -This route accepts in the request body an object that has the data necessary for the provider to update the user's password. +## Line Items -For the `emailpass` provider, you must pass the following properties: +A line item, represented by the [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model, is a quantity of a product variant added to the cart. A cart has multiple line items. -- `email`: The user's email. -- `password`: The new password. +A line item stores some of the product variant’s properties, such as the `product_title` and `product_description`. It also stores data related to the item’s quantity and price. -### Response Fields +In the Medusa application, a product variant is implemented in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). -If the authentication is successful, the request returns an object with a `success` property set to `true`: +*** -```json -{ - "success": "true" -} -``` +## Shipping Methods +A shipping method, represented by the [ShippingMethod data model](https://docs.medusajs.com/references/cart/models/ShippingMethod/index.html.md), is used to fulfill the items in the cart after the order is placed. A cart can have more than one shipping method. -# How to Create an Actor Type +In the Medusa application, the shipping method is created from a shipping option, available through the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md). Its ID is stored in the `shipping_option_id` property of the method. -In this document, learn how to create an actor type and authenticate its associated data model. +### data Property -## 0. Create Module with Data Model +After an order is placed, you can use a third-party fulfillment provider to fulfill its shipments. -Before creating an actor type, you must have a module with a data model representing the actor type. +If the fulfillment provider requires additional custom data to be passed along from the checkout process, set this data in the `ShippingMethod`'s `data` property. -Learn how to create a module in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). +The `data` property is an object used to store custom data relevant later for fulfillment. -The rest of this guide uses this `Manager` data model as an example: -```ts title="src/modules/manager/models/manager.ts" -import { model } from "@medusajs/framework/utils" +# Tax Lines in Cart Module -const Manager = model.define("manager", { - id: model.id().primaryKey(), - firstName: model.text(), - lastName: model.text(), - email: model.text(), -}) +In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. -export default Manager -``` +## What are Tax Lines? -*** +A tax line indicates the tax rate of a line item or a shipping method. The [LineItemTaxLine data model](https://docs.medusajs.com/references/cart/models/LineItemTaxLine/index.html.md) represents a line item’s tax line, and the [ShippingMethodTaxLine data model](https://docs.medusajs.com/references/cart/models/ShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. -## 1. Create Workflow +![A diagram showcasing the relation between other data models and the tax line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534431/Medusa%20Resources/cart-tax-lines_oheaq6.jpg) -Start by creating a workflow that does two things: +*** -- Creates a record of the `Manager` data model. -- Sets the `app_metadata` property of the associated `AuthIdentity` record based on the new actor type. +## Tax Inclusivity -For example, create the file `src/workflows/create-manager.ts`. with the following content: +By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount, and then adding them to the item/method’s subtotal. -```ts title="src/workflows/create-manager.ts" highlights={workflowHighlights} -import { - createWorkflow, - createStep, - StepResponse, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { - setAuthAppMetadataStep, -} from "@medusajs/medusa/core-flows" -import ManagerModuleService from "../modules/manager/service" +However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. -type CreateManagerWorkflowInput = { - manager: { - first_name: string - last_name: string - email: string - } - authIdentityId: string -} +So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. -const createManagerStep = createStep( - "create-manager-step", - async ({ - manager: managerData, - }: Pick, - { container }) => { - const managerModuleService: ManagerModuleService = - container.resolve("managerModuleService") +The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective. - const manager = await managerModuleService.createManager( - managerData - ) +![A diagram showing an example of calculating the subtotal of a line item using its taxes](https://res.cloudinary.com/dza7lstvk/image/upload/v1711535295/Medusa%20Resources/cart-tax-inclusive_shpr3t.jpg) - return new StepResponse(manager) - } -) +For example, if a line item's amount is `5000`, the tax rate is `10`, and tax inclusivity is enabled, the tax amount is 10% of `5000`, which is `500`, making the unit price of the line item `4500`. -const createManagerWorkflow = createWorkflow( - "create-manager", - function (input: CreateManagerWorkflowInput) { - const manager = createManagerStep({ - manager: input.manager, - }) +*** - setAuthAppMetadataStep({ - authIdentityId: input.authIdentityId, - actorType: "manager", - value: manager.id, - }) +## Retrieve Tax Lines - return new WorkflowResponse(manager) - } -) +When using the Cart and Tax modules together, you can use the `getTaxLines` method of the Tax Module’s main service. It retrieves the tax lines for a cart’s line items and shipping methods. -export default createManagerWorkflow +```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", + }, + } +) ``` -This workflow accepts the manager’s data and the associated auth identity’s ID as inputs. The next sections explain how the auth identity ID is retrieved. +Then, use the returned tax lines to set the line items and shipping methods’ tax lines: -The workflow has two steps: +```ts +// set line item tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "line_item_id" in line) +) -1. Create the manager using the `createManagerStep`. -2. Set the `app_metadata` property of the associated auth identity using the `setAuthAppMetadataStep` from Medusa's core workflows. You specify the actor type `manager` in the `actorType` property of the step’s input. +// set shipping method tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "shipping_line_id" in line) +) +``` -*** -## 2. Define the Create API Route +# Promotions Adjustments in Carts -Next, you’ll use the workflow defined in the previous section in an API route that creates a manager. +In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. -So, create the file `src/api/manager/route.ts` with the following content: +## What are Adjustment Lines? -```ts title="src/api/manager/route.ts" highlights={createRouteHighlights} -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { MedusaError } from "@medusajs/framework/utils" -import createManagerWorkflow from "../../workflows/create-manager" +An adjustment line indicates a change to an item or a shipping method’s amount. It’s used to apply promotions or discounts on a cart. -type RequestBody = { - first_name: string - last_name: string - email: string -} +The [LineItemAdjustment](https://docs.medusajs.com/references/cart/models/LineItemAdjustment/index.html.md) data model represents changes on a line item, and the [ShippingMethodAdjustment](https://docs.medusajs.com/references/cart/models/ShippingMethodAdjustment/index.html.md) data model represents changes on a shipping method. -export async function POST( - req: AuthenticatedMedusaRequest, - 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." - ) - } +![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg) - const { result } = await createManagerWorkflow(req.scope) - .run({ - input: { - manager: req.body, - authIdentityId: req.auth_context.auth_identity_id, - }, - }) - - res.status(200).json({ manager: result }) -} -``` +The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. -Since the manager must be associated with an `AuthIdentity` record, the request is expected to be authenticated, even if the manager isn’t created yet. This can be achieved by: +*** -1. Obtaining a token usng the [/auth route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). -2. Passing the token in the bearer header of the request to this route. +## Discountable Option -In the API route, you create the manager using the workflow from the previous section and return it in the response. +The [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. + +When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules. *** -## 3. Apply the `authenticate` Middleware +## Promotion Actions -The last step is to apply the `authenticate` middleware on the API routes that require a manager’s authentication. +When using the Cart and Promotion modules together, such as in the Medusa application, use the [computeActions method of the Promotion Module’s main service](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). It retrieves the actions of line items and shipping methods. -To do that, create the file `src/api/middlewares.ts` with the following content: +Learn more about actions in the [Promotion Module’s documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md). -```ts title="src/api/middlewares.ts" highlights={middlewareHighlights} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" +For example: -export default defineMiddlewares({ - routes: [ - { - matcher: "/manager", - method: "POST", - middlewares: [ - authenticate("manager", ["session", "bearer"], { - allowUnregistered: true, - }), - ], - }, - { - matcher: "/manager/me*", - middlewares: [ - authenticate("manager", ["session", "bearer"]), - ], - }, +```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + ComputeActionAdjustmentLine, + ComputeActionItemLine, + ComputeActionShippingLine, + // ... +} from "@medusajs/framework/types" + +// retrieve the cart +const cart = await cartModuleService.retrieveCart("cart_123", { + relations: [ + "items.adjustments", + "shipping_methods.adjustments", ], }) -``` -This applies middlewares on two route patterns: +// 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, + }) + } +}) -1. The `authenticate` middleware is applied on the `/manager` API route for `POST` requests while allowing unregistered managers. This requires that a bearer token be passed in the request to access the manager’s auth identity but doesn’t require the manager to be registered. -2. The `authenticate` middleware is applied on all routes starting with `/manager/me`, restricting these routes to authenticated managers only. +// 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, + }) + } +}) -### Retrieve Manager API Route +// compute actions +const actions = await promotionModuleService.computeActions( + ["promo_123"], + { + items: lineItemAdjustments, + shipping_methods: shippingMethodAdjustments, + } +) +``` -For example, create the file `src/api/manager/me/route.ts` with the following content: +The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. -```ts title="src/api/manager/me/route.ts" -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import ManagerModuleService from "../../../modules/manager/service" +Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments. -export async function GET( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -): Promise { - const managerModuleService: ManagerModuleService = - req.scope.resolve("managerModuleService") +```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + AddItemAdjustmentAction, + AddShippingMethodAdjustment, + // ... +} from "@medusajs/framework/types" - const manager = await managerModuleService.retrieveManager( - req.auth_context.actor_id - ) +// ... - res.json({ manager }) -} +await cartModuleService.setLineItemAdjustments( + cart.id, + actions.filter( + (action) => action.action === "addItemAdjustment" + ) as AddItemAdjustmentAction[] +) + +await cartModuleService.setShippingMethodAdjustments( + cart.id, + actions.filter( + (action) => + action.action === "addShippingMethodAdjustment" + ) as AddShippingMethodAdjustment[] +) ``` -This route is only accessible by authenticated managers. You access the manager’s ID using `req.auth_context.actor_id`. -*** +# Fulfillment Concepts -## Test Custom Actor Type Authentication Flow +In this document, you’ll learn about some basic fulfillment concepts. -To authenticate managers: +## Fulfillment Set -1. Send a `POST` request to `/auth/manager/emailpass/register` to create an auth identity for the manager: +A fulfillment set is a general form or way of fulfillment. For example, shipping is a form of fulfillment, and pick-up is another form of fulfillment. Each of these can be created as fulfillment sets. -```bash -curl -X POST 'http://localhost:9000/auth/manager/emailpass/register' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "manager@gmail.com", - "password": "supersecret" -}' +A fulfillment set is represented by the [FulfillmentSet data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentSet/index.html.md). All other configurations, options, and management features are related to a fulfillment set, in one way or another. + +```ts +const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( + [ + { + name: "Shipping", + type: "shipping", + }, + { + name: "Pick-up", + type: "pick-up", + }, + ] +) ``` -Copy the returned token to use it in the next request. +*** -2. Send a `POST` request to `/manager` to create a manager: - -```bash -curl -X POST 'http://localhost:9000/manager' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data-raw '{ - "first_name": "John", - "last_name": "Doe", - "email": "manager@gmail.com" -}' -``` +## Service Zone -Replace `{token}` with the token returned in the previous step. +A service zone is a collection of geographical zones or areas. It’s used to restrict available shipping options to a defined set of locations. -3. Send a `POST` request to `/auth/manager/emailpass` again to retrieve an authenticated token for the manager: +A service zone is represented by the [ServiceZone data model](https://docs.medusajs.com/references/fulfillment/models/ServiceZone/index.html.md). It’s associated with a fulfillment set, as each service zone is specific to a form of fulfillment. For example, if a customer chooses to pick up items, you can restrict the available shipping options based on their location. -```bash -curl -X POST 'http://localhost:9000/auth/manager/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "manager@gmail.com", - "password": "supersecret" -}' -``` +![A diagram showcasing the relation between fulfillment sets, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712329770/Medusa%20Resources/service-zone_awmvfs.jpg) -4. You can now send authenticated requests as a manager. For example, send a `GET` request to `/manager/me` to retrieve the authenticated manager’s details: +A service zone can have multiple geographical zones, each represented by the [GeoZone data model](https://docs.medusajs.com/references/fulfillment/models/GeoZone/index.html.md). It holds location-related details to narrow down supported areas, such as country, city, or province code. -```bash -curl 'http://localhost:9000/manager/me' \ --H 'Authorization: Bearer {token}' -``` +*** -Whenever you want to log in as a manager, use the `/auth/manager/emailpass` API route, as explained in step 3. +## Shipping Profile -*** +A shipping profile defines a type of items that are shipped in a similar manner. For example, a `default` shipping profile is used for all item types, but the `digital` shipping profile is used for digital items that aren’t shipped and delivered conventionally. -## Delete User of Actor Type +A shipping profile is represented by the [ShippingProfile data model](https://docs.medusajs.com/references/fulfillment/models/ShippingProfile/index.html.md). It only defines the profile’s details, but it’s associated with the shipping options available for the item type. -When you delete a user of the actor type, you must update its auth identity to remove the association to the user. -For example, create the following workflow that deletes a manager and updates its auth identity, create the file `src/workflows/delete-manager.ts` with the following content: +# Links between Cart Module and Other Modules -```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import ManagerModuleService from "../modules/manager/service" +This document showcases the module links defined between the Cart Module and other commerce modules. -export type DeleteManagerWorkflow = { - id: string -} +## Summary -const deleteManagerStep = createStep( - "delete-manager-step", - async ( - { id }: DeleteManagerWorkflow, - { container }) => { - const managerModuleService: ManagerModuleService = - container.resolve("managerModuleService") +The Cart Module has the following links to other modules: - const manager = await managerModuleService.retrieve(id) +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - await managerModuleService.deleteManagers(id) +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|| in |Read-only|| +| in ||Stored|| +|| in |Stored|| +|| in |Read-only|| +|| in |Read-only|| +|| in |Stored|| +|| in |Read-only|| +|| in |Read-only|| - return new StepResponse(undefined, { manager }) - }, - async ({ manager }, { container }) => { - const managerModuleService: ManagerModuleService = - container.resolve("managerModuleService") +*** - await managerModuleService.createManagers(manager) - } - ) -``` +## Customer Module -You add a step that deletes the manager using the `deleteManagers` method of the module's main service. In the compensation function, you create the manager again. +Medusa defines a read-only link between the `Cart` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of a cart's customer, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model. -Next, in the same file, add the workflow that deletes a manager: +### Retrieve with Query -```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={deleteHighlights} -// other imports -import { MedusaError } from "@medusajs/framework/utils" -import { - WorkflowData, - WorkflowResponse, - createWorkflow, - transform, -} from "@medusajs/framework/workflows-sdk" -import { - setAuthAppMetadataStep, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" +To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: -// ... +### query.graph -export const deleteManagerWorkflow = createWorkflow( - "delete-manager", - ( - input: WorkflowData - ): WorkflowResponse => { - deleteManagerStep(input) +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "customer.*", + ], +}) - const { data: authIdentities } = useQueryGraphStep({ - entity: "auth_identity", - fields: ["id"], - filters: { - app_metadata: { - // the ID is of the format `{actor_type}_id`. - manager_id: input.id, - }, - }, - }) +// carts.order +``` - const authIdentity = transform( - { authIdentities }, - ({ authIdentities }) => { - const authIdentity = authIdentities[0] +### useQueryGraphStep - if (!authIdentity) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - "Auth identity not found" - ) - } +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - return authIdentity - } - ) +// ... - setAuthAppMetadataStep({ - authIdentityId: authIdentity.id, - actorType: "manager", - value: null, - }) +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "customer.*", + ], +}) - return new WorkflowResponse(input.id) - } -) +// carts.order ``` -In the workflow, you: +*** -1. Use the `deleteManagerStep` defined earlier to delete the manager. -2. Retrieve the auth identity of the manager using Query. To do that, you filter the `app_metadata` property of an auth identity, which holds the user's ID under `{actor_type_name}_id`. So, in this case, it's `manager_id`. -3. Check that the auth identity exist, then, update the auth identity to remove the ID of the manager from it. +## Order Module -You can use this workflow when deleting a manager, such as in an API route. +The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management features. +Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. -# Auth Module Options +![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg) -In this document, you'll learn about the options of the Auth Module. +### Retrieve with Query -## providers +To retrieve the order of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: -The `providers` option is an array of auth module providers. +### query.graph -When the Medusa application starts, these providers are registered and can be used to handle authentication. +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "order.*", + ], +}) -By default, the `emailpass` provider is registered to authenticate customers and admin users. +// carts.order +``` -For example: +### useQueryGraphStep -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/auth", - dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], - options: { - providers: [ - { - resolve: "@medusajs/medusa/auth-emailpass", - id: "emailpass", - options: { - // provider options... - }, - }, - ], - }, - }, +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "order.*", ], }) + +// carts.order ``` -The `providers` option is an array of objects that accept the following properties: +### Manage with Link -- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory. -- `id`: A string indicating the provider's unique name or ID. -- `options`: An optional object of the module provider's options. +To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -*** +### link.create -## Auth CORS +```ts +import { Modules } from "@medusajs/framework/utils" -The Medusa application's authentication API routes are defined under the `/auth` prefix that requires setting the `authCors` property of the `http` configuration. +// ... -By default, the Medusa application you created will have an `AUTH_CORS` environment variable, which is used as the value of `authCors`. +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.ORDER]: { + order_id: "order_123", + }, +}) +``` -Refer to [Medusa's configuration guide](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthCors/index.html.md) to learn more about the `authCors` configuration. +### createRemoteLinkStep -*** +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -## authMethodsPerActor Configuration +// ... -The Medusa application's configuration accept an `authMethodsPerActor` configuration which restricts the allowed auth providers used with an actor type. +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.ORDER]: { + order_id: "order_123", + }, +}) +``` -Learn more about the `authMethodsPerActor` configuration in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers#configure-allowed-auth-providers-of-actor-types/index.html.md). +*** +## Payment Module -# How to Handle Password Reset Token Event +The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) handles payment processing and management. -In this guide, you'll learn how to handle the `auth.password_reset` event, which is emitted when a request is sent to the [Generate Reset Password Token API route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#generate-reset-password-token-route/index.html.md). - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/reset-password/index.html.md) to learn how to reset your user admin password using the dashboard. +Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. -You'll create a subscriber that listens to the event. When the event is emitted, the subscriber sends an email notification to the user. +![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) -### Prerequisites +### Retrieve with Query -- [A notification provider module, such as SendGrid](https://docs.medusajs.com/architectural-modules/notification/sendgrid/index.html.md) +To retrieve the payment collection of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collection.*` in `fields`: -## 1. Create Subscriber +### query.graph -The first step is to create a subscriber that listens to the `auth.password_reset` and sends the user a notification with instructions to reset their password. +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "payment_collection.*", + ], +}) -Create the file `src/subscribers/handle-reset.ts` with the following content: +// carts.payment_collection +``` -```ts title="src/subscribers/handle-reset.ts" highlights={highlights} collapsibleLines="1-6" expandMoreLabel="Show Imports" -import { - SubscriberArgs, - type SubscriberConfig, -} from "@medusajs/medusa" -import { Modules } from "@medusajs/framework/utils" +### useQueryGraphStep -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 - ) +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - const urlPrefix = actor_type === "customer" ? - "https://storefront.com" : - "https://admin.com/app" +// ... - 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}`, - }, - }) -} +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "payment_collection.*", + ], +}) -export const config: SubscriberConfig = { - event: "auth.password_reset", -} +// carts.payment_collection ``` -You subscribe to the `auth.password_reset` event. The event has a data payload object with the following properties: +### Manage with Link -- `entity_id`: The identifier of the user. When using the `emailpass` provider, it's the user's email. -- `token`: The token to reset the user's password. -- `actor_type`: The user's actor type. For example, if the user is a customer, the `actor_type` is `customer`. If it's an admin user, the `actor_type` is `user`. +To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -This event's payload previously had an `actorType` field. It was renamed to `actor_type` after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). +### link.create -In the subscriber, you: +```ts +import { Modules } from "@medusajs/framework/utils" -- Decide the frontend URL based on whether the user is a customer or admin user by checking the value of `actor_type`. -- Resolve the Notification Module and use its `createNotifications` method to send the notification. -- You pass to the `createNotifications` method an object having the following properties: - - `to`: The identifier to send the notification to, which in this case is the email. - - `channel`: The channel to send the notification through, which in this case is email. - - `template`: The template ID in the third-party service. - - `data`: The data payload to pass to the template. You pass the URL to redirect the user to. You must pass the token and email in the URL so that the frontend can send them later to the Medusa application when reseting the password. +// ... -*** +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` -## 2. Test it Out: Generate Reset Password Token +### createRemoteLinkStep -To test the subscriber out, send a request to the `/auth/{actor_type}/{auth_provider}/reset-password` API route, replacing `{actor_type}` and `{auth_provider}` with the user's actor type and provider used for authentication respectively. +```ts +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -For example, to generate a reset password token for an admin user using the `emailpass` provider, send the following request: +// ... -```bash -curl --location 'http://localhost:9000/auth/user/emailpass/reset-password' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "identifier": "admin-test@gmail.com" -}' +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) ``` -In the request body, you must pass an `identifier` parameter. Its value is the user's identifier, which is the email in this case. - -If the token is generated successfully, the request returns a response with `201` status code. In the terminal, you'll find the following message indicating that the `auth.password_reset` event was emitted and your subscriber ran: - -```plain -info: Processing auth.password_reset which has 1 subscribers -``` +*** -The notification is sent to the user with the frontend URL to enter a new password. +## Product Module -*** +Medusa defines read-only links between: -## Next Steps: Implementing Frontend +- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `LineItem` data model. +- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `LineItem` data model. -In your frontend, you must have a page that accepts `token` and `email` query parameters. +### Retrieve with Query -The page shows the user password fields to enter their new password, then submits the new password, token, and email to the [Reset Password Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#reset-password-route/index.html.md). +To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: -### Examples +To retrieve the product, pass `product.*` in `fields`. -- [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) +### query.graph +```ts +const { data: lineItems } = await query.graph({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) -# Fulfillment Concepts +// lineItems.variant +``` -In this document, you’ll learn about some basic fulfillment concepts. +### useQueryGraphStep -## Fulfillment Set +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -A fulfillment set is a general form or way of fulfillment. For example, shipping is a form of fulfillment, and pick-up is another form of fulfillment. Each of these can be created as fulfillment sets. +// ... -A fulfillment set is represented by the [FulfillmentSet data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentSet/index.html.md). All other configurations, options, and management features are related to a fulfillment set, in one way or another. +const { data: lineItems } = useQueryGraphStep({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) -```ts -const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( - [ - { - name: "Shipping", - type: "shipping", - }, - { - name: "Pick-up", - type: "pick-up", - }, - ] -) +// lineItems.variant ``` *** -## Service Zone - -A service zone is a collection of geographical zones or areas. It’s used to restrict available shipping options to a defined set of locations. - -A service zone is represented by the [ServiceZone data model](https://docs.medusajs.com/references/fulfillment/models/ServiceZone/index.html.md). It’s associated with a fulfillment set, as each service zone is specific to a form of fulfillment. For example, if a customer chooses to pick up items, you can restrict the available shipping options based on their location. +## Promotion Module -![A diagram showcasing the relation between fulfillment sets, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712329770/Medusa%20Resources/service-zone_awmvfs.jpg) +The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features. -A service zone can have multiple geographical zones, each represented by the [GeoZone data model](https://docs.medusajs.com/references/fulfillment/models/GeoZone/index.html.md). It holds location-related details to narrow down supported areas, such as country, city, or province code. +Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart. -*** +![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) -## Shipping Profile +Medusa also defines a read-only link between the `LineItemAdjustment` and `Promotion` data models. This means you can retrieve the details of the promotion applied on a line item, but you don't manage the links in a pivot table in the database. The promotion of a line item is determined by the `promotion_id` property of the `LineItemAdjustment` data model. -A shipping profile defines a type of items that are shipped in a similar manner. For example, a `default` shipping profile is used for all item types, but the `digital` shipping profile is used for digital items that aren’t shipped and delivered conventionally. +### Retrieve with Query -A shipping profile is represented by the [ShippingProfile data model](https://docs.medusajs.com/references/fulfillment/models/ShippingProfile/index.html.md). It only defines the profile’s details, but it’s associated with the shipping options available for the item type. +To retrieve the promotions of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotions.*` in `fields`: +To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. -# Item Fulfillment +### query.graph -In this document, you’ll learn about the concepts of item fulfillment. +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "promotions.*", + ], +}) -## Fulfillment Data Model +// carts.promotions +``` -A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md). +### useQueryGraphStep -*** +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -## Fulfillment Processing by a Fulfillment Provider +// ... -A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items. +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "promotions.*", + ], +}) -The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. +// carts.promotions +``` -![A diagram showcasing the relation between a fulfillment, fulfillment provider, and shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331947/Medusa%20Resources/fulfillment-shipping-option_jk9ndp.jpg) +### Manage with Link -*** +To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -## data Property +### link.create -The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. +```ts +import { Modules } from "@medusajs/framework/utils" -For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details. +// ... -*** +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` -## Fulfillment Items +### createRemoteLinkStep -A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model. +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill. +// ... -![A diagram showcasing the relation between fulfillment and fulfillment items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712332114/Medusa%20Resources/fulfillment-item_etzxb0.jpg) +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +*** + +## Region Module + +Medusa defines a read-only link between the `Cart` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of a cart's region, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. + +### Retrieve with Query + +To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "region.*", + ], +}) + +// carts.region +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "region.*", + ], +}) + +// carts.region +``` + +*** + +## Sales Channel Module + +Medusa defines a read-only link between the `Cart` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of a cart's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. + +### Retrieve with Query + +To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) + +// carts.sales_channel +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) + +// carts.sales_channel +``` + + +# Fulfillment Module Provider + +In this document, you’ll learn what a fulfillment module provider is. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard. + +## What’s a Fulfillment Module Provider? + +A fulfillment module provider handles fulfilling items, typically using a third-party integration. + +Fulfillment module providers registered in the Fulfillment Module's [options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) are stored and represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md). + +*** + +## Configure Fulfillment Providers + +The Fulfillment Module accepts a `providers` option that allows you to register providers in your application. + +Learn more about the `providers` option in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md). + +*** + +## How to Create a Fulfillment Provider? + +Refer to [this guide](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) to learn how to create a fulfillment module provider. + + +# Item Fulfillment + +In this document, you’ll learn about the concepts of item fulfillment. + +## Fulfillment Data Model + +A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md). + +*** + +## Fulfillment Processing by a Fulfillment Provider + +A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items. + +The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. + +![A diagram showcasing the relation between a fulfillment, fulfillment provider, and shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331947/Medusa%20Resources/fulfillment-shipping-option_jk9ndp.jpg) + +*** + +## data Property + +The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. + +For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details. + +*** + +## Fulfillment Items + +A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model. + +The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill. + +![A diagram showcasing the relation between fulfillment and fulfillment items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712332114/Medusa%20Resources/fulfillment-item_etzxb0.jpg) *** @@ -21323,12 +21358,14 @@ This document showcases the module links defined between the Fulfillment Module The Fulfillment Module has the following links to other modules: -- [`Order` data model of the Order Module \<> `Fulfillment` data model](#order-module). -- [`Return` data model of the Order Module \<> `Fulfillment` data model](#order-module). -- [`PriceSet` data model of the Pricing Module \<> `ShippingOption` data model](#pricing-module). -- [`Product` data model of the Product Module \<> `ShippingProfile` data model](#product-module). -- [`StockLocation` data model of the Stock Location Module \<> `FulfillmentProvider` data model](#stock-location-module). -- [`StockLocation` data model of the Stock Location Module \<> `FulfillmentSet` data model](#stock-location-module). +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Stored|| +| in ||Stored|| +| in ||Stored|| +| in ||Stored|| +| in ||Stored|| +| in ||Stored|| *** @@ -21673,33 +21710,6 @@ createRemoteLinkStep({ ``` -# Fulfillment Module Provider - -In this document, you’ll learn what a fulfillment module provider is. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard. - -## What’s a Fulfillment Module Provider? - -A fulfillment module provider handles fulfilling items, typically using a third-party integration. - -Fulfillment module providers registered in the Fulfillment Module's [options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) are stored and represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md). - -*** - -## Configure Fulfillment Providers - -The Fulfillment Module accepts a `providers` option that allows you to register providers in your application. - -Learn more about the `providers` option in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md). - -*** - -## How to Create a Fulfillment Provider? - -Refer to [this guide](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) to learn how to create a fulfillment module provider. - - # Fulfillment Module Options In this document, you'll learn about the options of the Fulfillment Module. @@ -21745,6 +21755,49 @@ The `providers` option is an array of objects that accept the following properti - `options`: An optional object of the module provider's options. +# Inventory Concepts + +In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. + +## InventoryItem + +An inventory item, represented by the [InventoryItem data model](https://docs.medusajs.com/references/inventory-next/models/InventoryItem/index.html.md), is a stock-kept item, such as a product, whose inventory can be managed. + +The `InventoryItem` data model mainly holds details related to the underlying stock item, but has relations to other data models that include its inventory details. + +![A diagram showcasing the relation between data models in the Inventory Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658103/Medusa%20Resources/inventory-architecture_kxr2ql.png) + +### Inventory Shipping Requirement + +An inventory item has a `requires_shipping` field (enabled by default) that indicates whether the item requires shipping. For example, if you're selling a digital license that has limited stock quantity but doesn't require shipping. + +When a product variant is purchased in the Medusa application, this field is used to determine whether the item requires shipping. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md). + +*** + +## InventoryLevel + +An inventory level, represented by the [InventoryLevel data model](https://docs.medusajs.com/references/inventory-next/models/InventoryLevel/index.html.md), holds the inventory and quantity details of an inventory item in a specific location. + +It has three quantity-related properties: + +- `stocked_quantity`: The available stock quantity of an item in the associated location. +- `reserved_quantity`: The quantity reserved from the available `stocked_quantity`. It indicates the quantity that's still not removed from stock, but considered as unavailable when checking whether an item is in stock. +- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This property doesn't play into the `stocked_quantity` or when checking whether an item is in stock. + +### Associated Location + +The inventory level's location is determined by the `location_id` property. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the Stock Location Module. + +*** + +## ReservationItem + +A reservation item, represented by the [ReservationItem](https://docs.medusajs.com/references/inventory-next/models/ReservationItem/index.html.md) data model, represents unavailable quantity of an inventory item in a location. It's used when an order is placed but not fulfilled yet. + +The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. + + # Shipping Option In this document, you’ll learn about shipping options and their rules. @@ -22258,77 +22311,36 @@ The bundled product has the same inventory items as those of the products part o You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). -# Inventory Concepts - -In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. - -## InventoryItem - -An inventory item, represented by the [InventoryItem data model](https://docs.medusajs.com/references/inventory-next/models/InventoryItem/index.html.md), is a stock-kept item, such as a product, whose inventory can be managed. +# Links between Inventory Module and Other Modules -The `InventoryItem` data model mainly holds details related to the underlying stock item, but has relations to other data models that include its inventory details. +This document showcases the module links defined between the Inventory Module and other commerce modules. -![A diagram showcasing the relation between data models in the Inventory Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658103/Medusa%20Resources/inventory-architecture_kxr2ql.png) +## Summary -### Inventory Shipping Requirement +The Inventory Module has the following links to other modules: -An inventory item has a `requires_shipping` field (enabled by default) that indicates whether the item requires shipping. For example, if you're selling a digital license that has limited stock quantity but doesn't require shipping. +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -When a product variant is purchased in the Medusa application, this field is used to determine whether the item requires shipping. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md). +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Stored|| +|| in |Read-only|| *** -## InventoryLevel +## Product Module -An inventory level, represented by the [InventoryLevel data model](https://docs.medusajs.com/references/inventory-next/models/InventoryLevel/index.html.md), holds the inventory and quantity details of an inventory item in a specific location. +Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. -It has three quantity-related properties: +![A diagram showcasing an example of how data models from the Inventory and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658720/Medusa%20Resources/inventory-product_ejnray.jpg) -- `stocked_quantity`: The available stock quantity of an item in the associated location. -- `reserved_quantity`: The quantity reserved from the available `stocked_quantity`. It indicates the quantity that's still not removed from stock, but considered as unavailable when checking whether an item is in stock. -- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This property doesn't play into the `stocked_quantity` or when checking whether an item is in stock. +A product variant whose `manage_inventory` property is enabled has an associated inventory item. Through that inventory's items relations in the Inventory Module, you can manage and check the variant's inventory quantity. -### Associated Location +Learn more about product variant's inventory management in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). -The inventory level's location is determined by the `location_id` property. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the Stock Location Module. +### Retrieve with Query -*** - -## ReservationItem - -A reservation item, represented by the [ReservationItem](https://docs.medusajs.com/references/inventory-next/models/ReservationItem/index.html.md) data model, represents unavailable quantity of an inventory item in a location. It's used when an order is placed but not fulfilled yet. - -The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. - - -# Links between Inventory Module and Other Modules - -This document showcases the module links defined between the Inventory Module and other commerce modules. - -## Summary - -The Inventory Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -- [`ProductVariant` data model of Product Module \<> `InventoryItem` data model](#product-module). -- [`InventoryLevel` data model \<> `StockLocation` data model of Stock Location Module](#stock-location-module). (Read-only). - -*** - -## Product Module - -Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. - -![A diagram showcasing an example of how data models from the Inventory and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658720/Medusa%20Resources/inventory-product_ejnray.jpg) - -A product variant whose `manage_inventory` property is enabled has an associated inventory item. Through that inventory's items relations in the Inventory Module, you can manage and check the variant's inventory quantity. - -Learn more about product variant's inventory management in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). - -### Retrieve with Query - -To retrieve the product variants of an inventory item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variants.*` in `fields`: +To retrieve the product variants of an inventory item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variants.*` in `fields`: ### query.graph @@ -22489,115 +22501,115 @@ An order can have multiple transactions. The sum of these transactions must be e Learn more about transactions in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions/index.html.md). -# Order Edit - -In this document, you'll learn about order edits. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/edit/index.html.md) to learn how to edit an order's items using the dashboard. +# Order Claim -## What is an Order Edit? +In this document, you’ll learn about order claims. -A merchant can edit an order to add new items or change the quantity of existing items in the order. +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/claims/index.html.md) to learn how to manage an order's claims using the dashboard. -An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md). +## What is a Claim? -The `OrderChange` data model is associated with any type of change, including a return or exchange. However, its `change_type` property distinguishes the type of change it's making. +When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. -In the case of an order edit, the `OrderChange`'s type is `edit`. +The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. *** -## Add Items in an Order Edit - -When the merchant adds new items to the order in the order edit, the item is added as an [OrderItem](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). +## Claim Type -Also, an `OrderChangeAction` is created. The [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md) represents a change made by an `OrderChange`, such as an item added. +The `Claim` data model has a `type` property whose value indicates the type of the claim: -So, when an item is added, an `OrderChangeAction` is created with the type `ITEM_ADD`. In its `details` property, the item's ID, price, and quantity are stored. +- `refund`: the items are returned, and the customer is refunded. +- `replace`: the items are returned, and the customer receives new items. *** -## Update Items in an Order Edit +## Old and Replacement Items -A merchant can update an existing item's quantity or price. +When the claim is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is also created to handle receiving the old items from the customer. -This change is added as an `OrderChangeAction` with the type `ITEM_UPDATE`. In its `details` property, the item's ID, new price, and new quantity are stored. +Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). + +If the claim’s type is `replace`, replacement items are represented by the [ClaimItem data model](https://docs.medusajs.com/references/order/models/OrderClaimItem/index.html.md). *** -## Shipping Methods of New Items in the Edit +## Claim Shipping Methods -Adding new items to the order requires adding shipping methods for those items. +A claim uses shipping methods to send the replacement items to the customer. These methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). -These shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). Also, an `OrderChangeAction` is created with the type `SHIPPING_ADD` +The shipping methods for the returned items are associated with the claim's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md). *** -## How Order Edits Impact an Order’s Version +## Claim Refund -When an order edit is confirmed, the order’s version is incremented. +If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property. + +The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim. *** -## Payments and Refunds for Order Edit Changes +## How Claims Impact an Order’s Version -Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order. +When a claim is confirmed, the order’s version is incremented. -This is determined by the comparison between the `OrderSummary` and the order's transactions, as mentioned in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions#checking-outstanding-amount/index.html.md). +# Order Edit -# Order Claim +In this document, you'll learn about order edits. -In this document, you’ll learn about order claims. +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/edit/index.html.md) to learn how to edit an order's items using the dashboard. -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/claims/index.html.md) to learn how to manage an order's claims using the dashboard. +## What is an Order Edit? -## What is a Claim? +A merchant can edit an order to add new items or change the quantity of existing items in the order. -When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. +An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md). -The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. +The `OrderChange` data model is associated with any type of change, including a return or exchange. However, its `change_type` property distinguishes the type of change it's making. + +In the case of an order edit, the `OrderChange`'s type is `edit`. *** -## Claim Type +## Add Items in an Order Edit -The `Claim` data model has a `type` property whose value indicates the type of the claim: +When the merchant adds new items to the order in the order edit, the item is added as an [OrderItem](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). -- `refund`: the items are returned, and the customer is refunded. -- `replace`: the items are returned, and the customer receives new items. +Also, an `OrderChangeAction` is created. The [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md) represents a change made by an `OrderChange`, such as an item added. -*** +So, when an item is added, an `OrderChangeAction` is created with the type `ITEM_ADD`. In its `details` property, the item's ID, price, and quantity are stored. -## Old and Replacement Items +*** -When the claim is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is also created to handle receiving the old items from the customer. +## Update Items in an Order Edit -Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). +A merchant can update an existing item's quantity or price. -If the claim’s type is `replace`, replacement items are represented by the [ClaimItem data model](https://docs.medusajs.com/references/order/models/OrderClaimItem/index.html.md). +This change is added as an `OrderChangeAction` with the type `ITEM_UPDATE`. In its `details` property, the item's ID, new price, and new quantity are stored. *** -## Claim Shipping Methods +## Shipping Methods of New Items in the Edit -A claim uses shipping methods to send the replacement items to the customer. These methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). +Adding new items to the order requires adding shipping methods for those items. -The shipping methods for the returned items are associated with the claim's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md). +These shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). Also, an `OrderChangeAction` is created with the type `SHIPPING_ADD` *** -## Claim Refund - -If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property. +## How Order Edits Impact an Order’s Version -The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim. +When an order edit is confirmed, the order’s version is incremented. *** -## How Claims Impact an Order’s Version +## Payments and Refunds for Order Edit Changes -When a claim is confirmed, the order’s version is incremented. +Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order. + +This is determined by the comparison between the `OrderSummary` and the order's transactions, as mentioned in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions#checking-outstanding-amount/index.html.md). # Order Exchange @@ -22663,17 +22675,19 @@ The Order Module has the following links to other modules: Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -- [`Order` data model \<> `Customer` data model of Customer Module](#customer-module). (Read-only). -- [`Order` data model \<> `Cart` data model of Cart Module](#cart-module). -- [`Order` data model \<> `Fulfillment` data model of Fulfillment Module](#fulfillment-module). -- [`Return` data model \<> `Fulfillment` data model of Fulfillment Module](#fulfillment-module). -- [`Order` data model \<> `PaymentCollection` data model of Payment Module](#payment-module). -- [`OrderClaim` data model \<> `PaymentCollection` data model of Payment Module](#payment-module). -- [`OrderExchange` data model \<> `PaymentCollection` data model of Payment Module](#payment-module). -- [`Order` data model \<> `Product` data model of Product Module](#product-module). (Read-only). -- [`Order` data model \<> `Promotion` data model of Promotion Module](#promotion-module). -- [`Order` data model \<> `Region` data model of Region Module](#region-module). (Read-only). -- [`Order` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). (Read-only). +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|| in |Read-only|| +|| in |Stored|| +|| in |Stored|| +|| in |Stored|| +|| in |Stored|| +|| in |Stored|| +|| in |Stored|| +|| in |Read-only|| +|| in |Stored|| +|| in |Read-only|| +|| in |Read-only|| *** @@ -23214,35 +23228,6 @@ The following table lists the possible `action` values that Medusa uses and what |\`WRITE\_OFF\_ITEM\`|Remove an item's quantity as part of the claim, without adding the quantity back to the item variant's inventory.|\`details\`| -# Tax Lines in Order Module - -In this document, you’ll learn about tax lines in an order. - -## What are Tax Lines? - -A tax line indicates the tax rate of a line item or a shipping method. - -The [OrderLineItemTaxLine data model](https://docs.medusajs.com/references/order/models/OrderLineItemTaxLine/index.html.md) represents a line item’s tax line, and the [OrderShippingMethodTaxLine data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. - -![A diagram showcasing the relation between orders, items and shipping methods, and tax lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307225/Medusa%20Resources/order-tax-lines_sixujd.jpg) - -*** - -## Tax Inclusivity - -By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount and then adding it to the item/method’s subtotal. - -However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. - -So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. - -The following diagram is a simplified showcase of how a subtotal is calculated from the tax perspective. - -![A diagram showcasing how a subtotal is calculated from the tax perspective](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307395/Medusa%20Resources/order-tax-inclusive_oebdnm.jpg) - -For example, if a line item's amount is `5000`, the tax rate is `10`, and `is_tax_inclusive` is enabled, the tax amount is 10% of `5000`, which is `500`. The item's unit price becomes `4500`. - - # Order Versioning In this document, you’ll learn how an order and its details are versioned. @@ -23456,6 +23441,35 @@ await orderModuleService.setOrderShippingMethodAdjustments( ``` +# Tax Lines in Order Module + +In this document, you’ll learn about tax lines in an order. + +## What are Tax Lines? + +A tax line indicates the tax rate of a line item or a shipping method. + +The [OrderLineItemTaxLine data model](https://docs.medusajs.com/references/order/models/OrderLineItemTaxLine/index.html.md) represents a line item’s tax line, and the [OrderShippingMethodTaxLine data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. + +![A diagram showcasing the relation between orders, items and shipping methods, and tax lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307225/Medusa%20Resources/order-tax-lines_sixujd.jpg) + +*** + +## Tax Inclusivity + +By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount and then adding it to the item/method’s subtotal. + +However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. + +So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. + +The following diagram is a simplified showcase of how a subtotal is calculated from the tax perspective. + +![A diagram showcasing how a subtotal is calculated from the tax perspective](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307395/Medusa%20Resources/order-tax-inclusive_oebdnm.jpg) + +For example, if a line item's amount is `5000`, the tax rate is `10`, and `is_tax_inclusive` is enabled, the tax amount is 10% of `5000`, which is `500`. The item's unit price becomes `4500`. + + # Transactions In this document, you’ll learn about an order’s transactions and its use. @@ -23504,258 +23518,169 @@ The `OrderTransaction` data model has two properties that determine which data m - `reference_id`: indicates the ID of the record in the table. For example, `pay_123`. -# Pricing Concepts +# Account Holders and Saved Payment Methods -In this document, you’ll learn about the main concepts in the Pricing Module. +In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. -## Price Set +Account holders are available starting from Medusa `v2.5.0`. -A [PriceSet](https://docs.medusajs.com/references/pricing/models/PriceSet/index.html.md) represents a collection of prices that are linked to a resource (for example, a product or a shipping option). +## What's an Account Holder? -Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). +An account holder represents a customer that can have saved payment methods in a third-party service. It's represented by the `AccountHolder` data model. -![A diagram showcasing the relation between the price set and price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648650/Medusa%20Resources/price-set-money-amount_xeees0.jpg) +It holds fields retrieved from the third-party provider, such as: -*** +- `external_id`: The ID of the equivalent customer or account holder in the third-party provider. +- `data`: Data returned by the payment provider when the account holder is created. -## Price List +A payment provider that supports saving payment methods for customers would create the equivalent of an account holder in the third-party provider. Then, whenever a payment method is saved, it would be saved under the account holder in the third-party provider. -A [PriceList](https://docs.medusajs.com/references/pricing/models/PriceList/index.html.md) is a group of prices only enabled if their conditions and rules are satisfied. +*** -A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied. +## Save Payment Methods -Its associated prices are represented by the `Price` data model. +If a payment provider supports saving payment methods for a customer, they must implement the following methods: +- `createAccountHolder`: Creates an account holder in the payment provider. The Payment Module uses this method before creating the account holder in Medusa, and uses the returned data to set fields like `external_id` and `data` in the created `AccountHolder` record. +- `deleteAccountHolder`: Deletes an account holder in the payment provider. The Payment Module uses this method when an account holder is deleted in Medusa. +- `savePaymentMethod`: Saves a payment method for an account holder in the payment provider. +- `listPaymentMethods`: Lists saved payment methods in the third-party service for an account holder. This is useful when displaying the customer's saved payment methods in the storefront. -# Prices Calculation +Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). -In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service. +*** -## calculatePrices Method +## Account Holder in Medusa Payment Flows -The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts as parameters the ID of one or more price sets and a context. +In the Medusa application, when a payment session is created for a registered customer, the Medusa application uses the Payment Module to create an account holder for the customer. -It returns a price object with the best matching price for each price set. +Consequently, the Payment Module uses the payment provider to create an account holder in the third-party service, then creates the account holder in Medusa. -### Calculation Context +This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). -The calculation context is an optional object passed as a second parameter to the `calculatePrices` method. It accepts rules to restrict the selected prices in the price set. -For example: +# Links between Payment Module and Other Modules -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSetId] }, - { - context: { - currency_code: currencyCode, - region_id: "reg_123", - }, - } -) -``` +This document showcases the module links defined between the Payment Module and other commerce modules. -In this example, you retrieve the prices in a price set for the specified currency code and region ID. +## Summary -### Returned Price Object +The Payment Module has the following links to other modules: -For each price set, the `calculatePrices` method selects two prices: +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Stored|| +| in ||Stored|| +| in ||Stored|| +| in ||Stored|| +| in ||Stored|| +| in ||Stored|| -- A calculated price: Either a price that belongs to a price list and best matches the specified context, or the same as the original price. -- An original price, which is either: - - The same price as the calculated price if the price list it belongs to is of type `override`; - - Or a price that doesn't belong to a price list and best matches the specified context. +*** -Both prices are returned in an object that has the following properties: - -- id: (\`string\`) The ID of the price set from which the price was selected. -- is\_calculated\_price\_price\_list: (\`boolean\`) Whether the calculated price belongs to a price list. -- calculated\_amount: (\`number\`) The amount of the calculated price, or \`null\` if there isn't a calculated price. This is the amount shown to the customer. -- is\_original\_price\_price\_list: (\`boolean\`) Whether the original price belongs to a price list. -- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful to compare with the \`calculated\_amount\`, such as to check for discounted value. -- currency\_code: (\`string\`) The currency code of the calculated price, or \`null\` if there isn't a calculated price. -- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) -- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) -- calculated\_price: (\`object\`) The calculated price's price details. - - - id: (\`string\`) The ID of the price. - - - price\_list\_id: (\`string\`) The ID of the associated price list. - - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. - - - min\_quantity: (\`number\`) The price's min quantity condition. - - - max\_quantity: (\`number\`) The price's max quantity condition. -- original\_price: (\`object\`) The original price's price details. - - - id: (\`string\`) The ID of the price. - - - price\_list\_id: (\`string\`) The ID of the associated price list. +## Cart Module - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. +The Cart Module provides cart-related features, but not payment processing. - - min\_quantity: (\`number\`) The price's min quantity condition. +Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. - - max\_quantity: (\`number\`) The price's max quantity condition. +Learn more about this relation in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection#usage-with-the-cart-module/index.html.md). -*** +### Retrieve with Query -## Examples +To retrieve the cart associated with the payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`: -Consider the following price set: +### query.graph ```ts -const priceSet = await pricingModuleService.createPriceSets({ - prices: [ - // default price - { - amount: 500, - currency_code: "EUR", - rules: {}, - }, - // prices with rules - { - amount: 400, - currency_code: "EUR", - rules: { - region_id: "reg_123", - }, - }, - { - amount: 450, - currency_code: "EUR", - rules: { - city: "krakow", - }, - }, - { - amount: 500, - currency_code: "EUR", - rules: { - city: "warsaw", - region_id: "reg_123", - }, - }, +const { data: paymentCollections } = await query.graph({ + entity: "payment_collection", + fields: [ + "cart.*", ], }) -``` -### Default Price Selection +// paymentCollections.cart +``` -### Code +### useQueryGraphStep ```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR" - } - } -) -``` - -### Result +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -### Calculate Prices with Rules +// ... -### Code +const { data: paymentCollections } = useQueryGraphStep({ + entity: "payment_collection", + fields: [ + "cart.*", + ], +}) -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR", - region_id: "reg_123", - city: "krakow" - } - } -) +// paymentCollections.cart ``` -### Result +### Manage with Link -### Price Selection with Price List +To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -### Code +### link.create ```ts -const priceList = pricingModuleService.createPriceLists([{ - title: "Summer Price List", - description: "Price list for summer sale", - starts_at: Date.parse("01/10/2023").toString(), - ends_at: Date.parse("31/10/2023").toString(), - rules: { - region_id: ['PL'] - }, - type: "sale", - prices: [ - { - amount: 400, - currency_code: "EUR", - price_set_id: priceSet.id, - }, - { - amount: 450, - currency_code: "EUR", - price_set_id: priceSet.id, - }, - ], -}]); - -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR", - region_id: "PL", - city: "krakow" - } - } -) -``` - -### Result +import { Modules } from "@medusajs/framework/utils" +// ... -# Links between Pricing Module and Other Modules +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` -This document showcases the module links defined between the Pricing Module and other commerce modules. +### createRemoteLinkStep -## Summary +```ts +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -The Pricing Module has the following links to other modules: +// ... -- [`ShippingOption` data model of Fulfillment Module \<> `PriceSet` data model](#fulfillment-module). -- [`ProductVariant` data model of Product Module \<> `PriceSet` data model](#product-module). +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` *** -## Fulfillment Module - -The Fulfillment Module provides fulfillment-related functionalities, including shipping options that the customer chooses from when they place their order. However, it doesn't provide pricing-related functionalities for these options. +## Customer Module -Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. +Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it. -![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) +This link is available starting from Medusa `v2.5.0`. ### Retrieve with Query -To retrieve the shipping option of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_option.*` in `fields`: +To retrieve the customer associated with an account holder with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: ### query.graph ```ts -const { data: priceSets } = await query.graph({ - entity: "price_set", +const { data: accountHolders } = await query.graph({ + entity: "account_holder", fields: [ - "shipping_option.*", + "customer.*", ], }) -// priceSets.shipping_option +// accountHolders.customer ``` ### useQueryGraphStep @@ -23765,19 +23690,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", +const { data: accountHolders } = useQueryGraphStep({ + entity: "account_holder", fields: [ - "shipping_option.*", + "customer.*", ], }) -// priceSets.shipping_option +// accountHolders.customer ``` ### Manage with Link -To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -23787,11 +23712,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", + [Modules.CUSTOMER]: { + customer_id: "cus_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", }, }) ``` @@ -23799,50 +23724,45 @@ await link.create({ ### createRemoteLinkStep ```ts -import { Modules } from "@medusajs/framework/utils" import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", + [Modules.CUSTOMER]: { + customer_id: "cus_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", }, }) ``` *** -## Product Module - -The Product Module doesn't store or manage the prices of product variants. - -Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variant’s prices are stored as prices belonging to a price set. +## Order Module -![A diagram showcasing an example of how data models from the Pricing and Product Module are linked. The PriceSet is linked to the ProductVariant of the Product Module.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651039/Medusa%20Resources/pricing-product_m4xaut.jpg) +An order's payment details are stored in a payment collection. This also applies for claims and exchanges. -So, when you want to add prices for a product variant, you create a price set and add the prices to it. +So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models. -You can then benefit from adding rules to prices or using the `calculatePrices` method to retrieve the price of a product variant within a specified context. +![A diagram showcasing an example of how data models from the Order and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716554726/Medusa%20Resources/order-payment_ubdwok.jpg) ### Retrieve with Query -To retrieve the variant of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: +To retrieve the order of a payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: ### query.graph ```ts -const { data: priceSets } = await query.graph({ - entity: "price_set", +const { data: paymentCollections } = await query.graph({ + entity: "payment_collection", fields: [ - "variant.*", + "order.*", ], }) -// priceSets.variant +// paymentCollections.order ``` ### useQueryGraphStep @@ -23852,19 +23772,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", +const { data: paymentCollections } = useQueryGraphStep({ + entity: "payment_collection", fields: [ - "variant.*", + "order.*", ], }) -// priceSets.variant +// paymentCollections.order ``` ### Manage with Link -To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the payment collections of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -23874,11 +23794,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", }, }) ``` @@ -23892,949 +23812,866 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", }, }) ``` +*** -# Price Rules - -In this document, you'll learn about price rules for price sets and price lists. +## Region Module -## Price Rule +You can specify for each region which payment providers are available. The Medusa application defines a link between the `PaymentProvider` and the `Region` data models. -You can restrict prices by rules. Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md). +![A diagram showcasing an example of how resources from the Payment and Region modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711569520/Medusa%20Resources/payment-region_jyo2dz.jpg) -The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price. +This increases the flexibility of your store. For example, you only show during checkout the payment providers associated with the cart's region. -For exmaple, you create a price restricted to `10557` zip codes. +### Retrieve with Query -![A diagram showcasing the relation between the PriceRule and Price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648772/Medusa%20Resources/price-rule-1_vy8bn9.jpg) +To retrieve the regions of a payment provider with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `regions.*` in `fields`: -A price can have multiple price rules. +### query.graph -For example, a price can be restricted by a region and a zip code. +```ts +const { data: paymentProviders } = await query.graph({ + entity: "payment_provider", + fields: [ + "regions.*", + ], +}) -![A diagram showcasing the relation between the PriceRule and Price with multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709649296/Medusa%20Resources/price-rule-3_pwpocz.jpg) +// paymentProviders.regions +``` -*** +### useQueryGraphStep -## Price List Rules +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md). +// ... -The `rules_count` property of a `PriceList` indicates how many rules are applied to it. +const { data: paymentProviders } = useQueryGraphStep({ + entity: "payment_provider", + fields: [ + "regions.*", + ], +}) -![A diagram showcasing the relation between the PriceSet, PriceList, Price, RuleType, and PriceListRuleValue](https://res.cloudinary.com/dza7lstvk/image/upload/v1709641999/Medusa%20Resources/price-list_zd10yd.jpg) +// paymentProviders.regions +``` +### Manage with Link -# Tax-Inclusive Pricing +To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. +### link.create -## What is Tax-Inclusive Pricing? +```ts +import { Modules } from "@medusajs/framework/utils" -A tax-inclusive price is a price of a resource that includes taxes. Medusa calculates the tax amount from the price rather than adds the amount to it. +// ... -For example, if a product’s price is $50, the tax rate is 2%, and tax-inclusive pricing is enabled, then the product's price is $49, and the applied tax amount is $1. +await link.create({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` -*** +### createRemoteLinkStep -## How is Tax-Inclusive Pricing Set? +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -The [PricePreference data model](https://docs.medusajs.com/references/pricing/models/PricePreference/index.html.md) holds the tax-inclusive setting for a context. It has two properties that indicate the context: +// ... -- `attribute`: The name of the attribute to compare against. For example, `region_id` or `currency_code`. -- `value`: The attribute’s value. For example, `reg_123` or `usd`. +createRemoteLinkStep({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` -Only `region_id` and `currency_code` are supported as an `attribute` at the moment. -The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context. +# Payment -For example: +In this document, you’ll learn what a payment is and how it's created, captured, and refunded. -```json -{ - "attribute": "currency_code", - "value": "USD", - "is_tax_inclusive": true, -} -``` +## What's a Payment? -In this example, tax-inclusivity is enabled for the `USD` currency code. +When a payment session is authorized, a payment, represented by the [Payment data model](https://docs.medusajs.com/references/payment/models/Payment/index.html.md), is created. This payment can later be captured or refunded. -*** +A payment carries many of the data and relations of a payment session: -## Tax-Inclusive Pricing in Price Calculation +- It belongs to the same payment collection. +- It’s associated with the same payment provider, which handles further payment processing. +- It stores the payment session’s `data` property in its `data` property, as it’s still useful for the payment provider’s processing. -### Tax Context +*** -As mentioned in the [Price Calculation documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), The `calculatePrices` method accepts as a parameter a calculation context. +## Capture Payments -To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context. +When a payment is captured, a capture, represented by the [Capture data model](https://docs.medusajs.com/references/payment/models/Capture/index.html.md), is created. It holds details related to the capture, such as the amount, the capture date, and more. -### Returned Tax Properties +The payment can also be captured incrementally, each time a capture record is created for that amount. -The `calculatePrices` method returns two properties related to tax-inclusivity: +![A diagram showcasing how a payment's multiple captures are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565445/Medusa%20Resources/payment-capture_f5fve1.jpg) -Learn more about the returned properties in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). +*** -- `is_calculated_price_tax_inclusive`: Whether the selected `calculated_price` is tax-inclusive. -- `is_original_price_tax_inclusive` : Whether the selected `original_price` is tax-inclusive. +## Refund Payments -A price is considered tax-inclusive if: +When a payment is refunded, a refund, represented by the [Refund data model](https://docs.medusajs.com/references/payment/models/Refund/index.html.md), is created. It holds details related to the refund, such as the amount, refund date, and more. -1. It belongs to the region or currency code specified in the calculation context; -2. and the region or currency code has a price preference with `is_tax_inclusive` enabled. +A payment can be refunded multiple times, and each time a refund record is created. -### Tax Context Precedence +![A diagram showcasing how a payment's multiple refunds are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565555/Medusa%20Resources/payment-refund_lgfvyy.jpg) -A region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if: -- both the `region_id` and `currency_code` are provided in the calculation context; -- the selected price belongs to the region; -- and the region has a price preference +# Payment Module Options +In this document, you'll learn about the options of the Payment Module. -# Account Holders and Saved Payment Methods +## All Module Options -In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`webhook\_delay\`|A number indicating the delay in milliseconds before processing a webhook event.|No|\`5000\`| +|\`webhook\_retries\`|The number of times to retry the webhook event processing in case of an error.|No|\`3\`| +|\`providers\`|An array of payment providers to install and register. Learn more |No|-| -Account holders are available starting from Medusa `v2.5.0`. +*** -## What's an Account Holder? +## providers Option -An account holder represents a customer that can have saved payment methods in a third-party service. It's represented by the `AccountHolder` data model. +The `providers` option is an array of payment module providers. -It holds fields retrieved from the third-party provider, such as: +When the Medusa application starts, these providers are registered and can be used to process payments. -- `external_id`: The ID of the equivalent customer or account holder in the third-party provider. -- `data`: Data returned by the payment provider when the account holder is created. +For example: -A payment provider that supports saving payment methods for customers would create the equivalent of an account holder in the third-party provider. Then, whenever a payment method is saved, it would be saved under the account holder in the third-party provider. +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" -*** +// ... -## Save Payment Methods +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/payment", + options: { + providers: [ + { + resolve: "@medusajs/medusa/payment-stripe", + id: "stripe", + options: { + // ... + }, + }, + ], + }, + }, + ], +}) +``` -If a payment provider supports saving payment methods for a customer, they must implement the following methods: +The `providers` option is an array of objects that accept the following properties: -- `createAccountHolder`: Creates an account holder in the payment provider. The Payment Module uses this method before creating the account holder in Medusa, and uses the returned data to set fields like `external_id` and `data` in the created `AccountHolder` record. -- `deleteAccountHolder`: Deletes an account holder in the payment provider. The Payment Module uses this method when an account holder is deleted in Medusa. -- `savePaymentMethod`: Saves a payment method for an account holder in the payment provider. -- `listPaymentMethods`: Lists saved payment methods in the third-party service for an account holder. This is useful when displaying the customer's saved payment methods in the storefront. +- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory. +- `id`: A string indicating the provider's unique name or ID. +- `options`: An optional object of the module provider's options. -Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). -*** +# Payment Collection -## Account Holder in Medusa Payment Flows +In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module. -In the Medusa application, when a payment session is created for a registered customer, the Medusa application uses the Payment Module to create an account holder for the customer. +## What's a Payment Collection? -Consequently, the Payment Module uses the payment provider to create an account holder in the third-party service, then creates the account holder in Medusa. +A payment collection stores payment details related to a resource, such as a cart or an order. It’s represented by the [PaymentCollection data model](https://docs.medusajs.com/references/payment/models/PaymentCollection/index.html.md). -This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). +Every purchase or request for payment starts with a payment collection. The collection holds details necessary to complete the payment, including: +- The [payment sessions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) that represents the payment amount to authorize. +- The [payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md) that are created when a payment session is authorized. They can be captured and refunded. +- The [payment providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) that handle the processing of each payment session, including the authorization, capture, and refund. -# Links between Payment Module and Other Modules +*** -This document showcases the module links defined between the Payment Module and other commerce modules. +## Multiple Payments -## Summary +The payment collection supports multiple payment sessions and payments. -The Payment Module has the following links to other modules: +You can use this to accept payments in increments or split payments across payment providers. -- [`Cart` data model of Cart Module \<> `PaymentCollection` data model](#cart-module). -- [`Customer` data model of Customer Module \<> `AccountHolder` data model](#customer-module). -- [`Order` data model of Order Module \<> `PaymentCollection` data model](#order-module). -- [`OrderClaim` data model of Order Module \<> `PaymentCollection` data model](#order-module). -- [`OrderExchange` data model of Order Module \<> `PaymentCollection` data model](#order-module). -- [`Region` data model of Region Module \<> `PaymentProvider` data model](#region-module). +![Diagram showcasing how a payment collection can have multiple payment sessions and payments](https://res.cloudinary.com/dza7lstvk/image/upload/v1711554695/Medusa%20Resources/payment-collection-multiple-payments_oi3z3n.jpg) *** -## Cart Module +## Usage with the Cart Module -The Cart Module provides cart-related features, but not payment processing. +The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment. -Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. +During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing. -Learn more about this relation in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection#usage-with-the-cart-module/index.html.md). +It also implements the payment flow during checkout as explained in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md). -### Retrieve with Query +![Diagram showcasing the relation between the Payment and Cart modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) -To retrieve the cart associated with the payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`: -### query.graph +# Accept Payment Flow -```ts -const { data: paymentCollections } = await query.graph({ - entity: "payment_collection", - fields: [ - "cart.*", - ], -}) +In this document, you’ll learn how to implement an accept-payment flow using workflows or the Payment Module's main service. -// paymentCollections.cart -``` +It's highly recommended to use Medusa's workflows to implement this flow. Use the Payment Module's main service for more complex cases. -### useQueryGraphStep +For a guide on how to implement this flow in the storefront, check out [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/index.html.md). -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +## Flow Overview -// ... +![A diagram showcasing the payment flow's steps](https://res.cloudinary.com/dza7lstvk/image/upload/v1711566781/Medusa%20Resources/payment-flow_jblrvw.jpg) -const { data: paymentCollections } = useQueryGraphStep({ - entity: "payment_collection", - fields: [ - "cart.*", - ], -}) +*** -// paymentCollections.cart -``` +## 1. Create a Payment Collection -### Manage with Link +A payment collection holds all details related to a resource’s payment operations. So, you start off by creating a payment collection. -To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +For example: -### link.create +### Using Workflow ```ts -import { Modules } from "@medusajs/framework/utils" +import { createPaymentCollectionForCartWorkflow } from "@medusajs/medusa/core-flows" // ... -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) +await createPaymentCollectionForCartWorkflow(req.scope) + .run({ + input: { + cart_id: "cart_123", + }, + }) ``` -### createRemoteLinkStep +### Using Service ```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) +const paymentCollection = + await paymentModuleService.createPaymentCollections({ + currency_code: "usd", + amount: 5000, + }) ``` *** -## Customer Module - -Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it. +## 2. Create Payment Sessions -This link is available starting from Medusa `v2.5.0`. +The payment collection has one or more payment sessions, each being a payment amount to be authorized by a payment provider. -### Retrieve with Query +So, after creating the payment collection, create at least one payment session for a provider. -To retrieve the customer associated with an account holder with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: +For example: -### query.graph +### Using Workflow ```ts -const { data: accountHolders } = await query.graph({ - entity: "account_holder", - fields: [ - "customer.*", - ], -}) +import { createPaymentSessionsWorkflow } from "@medusajs/medusa/core-flows" -// accountHolders.customer +// ... + +const { result: paymentSesion } = await createPaymentSessionsWorkflow(req.scope) + .run({ + input: { + payment_collection_id: "paycol_123", + provider_id: "stripe", + }, + }) ``` -### useQueryGraphStep +### Using Service ```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... +const paymentSession = + await paymentModuleService.createPaymentSession( + paymentCollection.id, + { + provider_id: "stripe", + currency_code: "usd", + amount: 5000, + data: { + // any necessary data for the + // payment provider + }, + } + ) +``` -const { data: accountHolders } = useQueryGraphStep({ - entity: "account_holder", - fields: [ - "customer.*", - ], -}) +*** -// accountHolders.customer -``` +## 3. Authorize Payment Session -### Manage with Link +Once the customer chooses a payment session, start the authorization process. This may involve some action performed by the third-party payment provider, such as entering a 3DS code. -To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +For example: -### link.create +### Using Step ```ts -import { Modules } from "@medusajs/framework/utils" +import { authorizePaymentSessionStep } from "@medusajs/medusa/core-flows" // ... -await link.create({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, +authorizePaymentSessionStep({ + id: "payses_123", + context: {}, }) ``` -### createRemoteLinkStep +### Using Service ```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, +const payment = authorizePaymentSessionStep({ + id: "payses_123", + context: {}, }) ``` -*** - -## Order Module - -An order's payment details are stored in a payment collection. This also applies for claims and exchanges. +When the payment authorization is successful, a payment is created and returned. -So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models. +### Handling Additional Action -![A diagram showcasing an example of how data models from the Order and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716554726/Medusa%20Resources/order-payment_ubdwok.jpg) +If you used the `authorizePaymentSessionStep`, you don't need to implement this logic as it's implemented in the step. -### Retrieve with Query +If the payment authorization isn’t successful, whether because it requires additional action or for another reason, the method updates the payment session with the new status and throws an error. -To retrieve the order of a payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: +In that case, you can catch that error and, if the session's `status` property is `requires_more`, handle the additional action, then retry the authorization. -### query.graph +For example: ```ts -const { data: paymentCollections } = await query.graph({ - entity: "payment_collection", - fields: [ - "order.*", - ], -}) +try { + const payment = + await paymentModuleService.authorizePaymentSession( + paymentSession.id, + {} + ) +} catch (e) { + // retrieve the payment session again + const updatedPaymentSession = ( + await paymentModuleService.listPaymentSessions({ + id: [paymentSession.id], + }) + )[0] -// paymentCollections.order + if (updatedPaymentSession.status === "requires_more") { + // TODO perform required action + // TODO authorize payment again. + } +} ``` -### useQueryGraphStep +*** -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +## 4. Payment Flow Complete -// ... +The payment flow is complete once the payment session is authorized and the payment is created. -const { data: paymentCollections } = useQueryGraphStep({ - entity: "payment_collection", - fields: [ - "order.*", - ], -}) +You can then: -// paymentCollections.order -``` +- Capture the payment either using the [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) or [capturePayment method](https://docs.medusajs.com/references/payment/capturePayment/index.html.md). +- Refund captured amounts using the [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) or [refundPayment method](https://docs.medusajs.com/references/payment/refundPayment/index.html.md). -### Manage with Link +Some payment providers allow capturing the payment automatically once it’s authorized. In that case, you don’t need to do it manually. -To manage the payment collections of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -### link.create +# Webhook Events -```ts -import { Modules } from "@medusajs/framework/utils" +In this document, you’ll learn how the Payment Module supports listening to webhook events. -// ... +## What's a Webhook Event? -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` +A webhook event is sent from a third-party payment provider to your application. It indicates a change in a payment’s status. -### createRemoteLinkStep +This is useful in many cases such as when a payment is being processed asynchronously or when a request is interrupted and the payment provider is sending details on the process later. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +*** -// ... +## getWebhookActionAndData Method -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` +The Payment Module’s main service has a [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/getWebhookActionAndData/index.html.md) used to handle incoming webhook events from third-party payment services. The method delegates the handling to the associated payment provider, which returns the event's details. -*** +Medusa implements a webhook listener route at the `/hooks/payment/[identifier]_[provider]` API route, where: -## Region Module +- `[identifier]` is the `identifier` static property defined in the payment provider. For example, `stripe`. +- `[provider]` is the ID of the provider. For example, `stripe`. -You can specify for each region which payment providers are available. The Medusa application defines a link between the `PaymentProvider` and the `Region` data models. +For example, when integrating basic Stripe payments with the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md), the webhook listener route is `/hooks/payment/stripe_stripe`. If you're integrating Stripe's Bancontact payments, the webhook listener route is `/hooks/payment/stripe-bancontact_stripe`. -![A diagram showcasing an example of how resources from the Payment and Region modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711569520/Medusa%20Resources/payment-region_jyo2dz.jpg) +Use that webhook listener in your third-party payment provider's configurations. -This increases the flexibility of your store. For example, you only show during checkout the payment providers associated with the cart's region. +![A diagram showcasing the steps of how the getWebhookActionAndData method words](https://res.cloudinary.com/dza7lstvk/image/upload/v1711567415/Medusa%20Resources/payment-webhook_seaocg.jpg) -### Retrieve with Query - -To retrieve the regions of a payment provider with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `regions.*` in `fields`: - -### query.graph - -```ts -const { data: paymentProviders } = await query.graph({ - entity: "payment_provider", - fields: [ - "regions.*", - ], -}) +If the event's details indicate that the payment should be authorized, then the [authorizePaymentSession method of the main service](https://docs.medusajs.com/references/payment/authorizePaymentSession/index.html.md) is executed on the specified payment session. -// paymentProviders.regions -``` +If the event's details indicate that the payment should be captured, then the [capturePayment method of the main service](https://docs.medusajs.com/references/payment/capturePayment/index.html.md) is executed on the payment of the specified payment session. -### useQueryGraphStep +### Actions After Webhook Payment Processing -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +After the payment webhook actions are processed and the payment is authorized or captured, the Medusa application completes the cart associated with the payment's collection if it's not completed yet. -// ... -const { data: paymentProviders } = useQueryGraphStep({ - entity: "payment_provider", - fields: [ - "regions.*", - ], -}) +# Payment Module Provider -// paymentProviders.regions -``` +In this document, you’ll learn what a payment module provider is. -### Manage with Link +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage the payment providers available in a region using the dashboard. -To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +## What's a Payment Module Provider? -### link.create +A payment module provider registers a payment provider that handles payment processing in the Medusa application. It integrates third-party payment providers, such as Stripe. -```ts -import { Modules } from "@medusajs/framework/utils" +To authorize a payment amount with a payment provider, a payment session is created and associated with that payment provider. The payment provider is then used to handle the authorization. -// ... +After the payment session is authorized, the payment provider is associated with the resulting payment and handles its payment processing, such as to capture or refund payment. -await link.create({ - [Modules.REGION]: { - region_id: "reg_123", - }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", - }, -}) -``` +### List of Payment Module Providers -### createRemoteLinkStep +- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md) -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +*** -// ... +## System Payment Provider -createRemoteLinkStep({ - [Modules.REGION]: { - region_id: "reg_123", - }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", - }, -}) -``` +The Payment Module provides a `system` payment provider that acts as a placeholder payment provider. +It doesn’t handle payment processing and delegates that to the merchant. It acts similarly to a cash-on-delivery (COD) payment method. -# Payment Module Options +*** -In this document, you'll learn about the options of the Payment Module. +## How are Payment Providers Created? -## All Module Options +A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`. -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`webhook\_delay\`|A number indicating the delay in milliseconds before processing a webhook event.|No|\`5000\`| -|\`webhook\_retries\`|The number of times to retry the webhook event processing in case of an error.|No|\`3\`| -|\`providers\`|An array of payment providers to install and register. Learn more |No|-| +Refer to [this guide](https://docs.medusajs.com/references/payment/provider/index.html.md) on how to create a payment provider for the Payment Module. *** -## providers Option - -The `providers` option is an array of payment module providers. - -When the Medusa application starts, these providers are registered and can be used to process payments. +## Configure Payment Providers -For example: +The Payment Module accepts a `providers` option that allows you to register providers in your application. -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +Learn more about this option in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options#providers/index.html.md). -// ... +*** -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/payment", - options: { - providers: [ - { - resolve: "@medusajs/medusa/payment-stripe", - id: "stripe", - options: { - // ... - }, - }, - ], - }, - }, - ], -}) -``` +## PaymentProvider Data Model -The `providers` option is an array of objects that accept the following properties: +When the Medusa application starts and registers the payment providers, it also creates a record of the `PaymentProvider` data model if none exists. -- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory. -- `id`: A string indicating the provider's unique name or ID. -- `options`: An optional object of the module provider's options. +This data model is used to reference a payment provider and determine whether it’s installed in the application. -# Accept Payment Flow +# Payment Session -In this document, you’ll learn how to implement an accept-payment flow using workflows or the Payment Module's main service. +In this document, you’ll learn what a payment session is. -It's highly recommended to use Medusa's workflows to implement this flow. Use the Payment Module's main service for more complex cases. +## What's a Payment Session? -For a guide on how to implement this flow in the storefront, check out [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/index.html.md). +A payment session, represented by the [PaymentSession data model](https://docs.medusajs.com/references/payment/models/PaymentSession/index.html.md), is a payment amount to be authorized. It’s associated with a payment provider that handles authorizing it. -## Flow Overview +A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. -![A diagram showcasing the payment flow's steps](https://res.cloudinary.com/dza7lstvk/image/upload/v1711566781/Medusa%20Resources/payment-flow_jblrvw.jpg) +![Diagram showcasing how every payment session has a different payment provider](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565056/Medusa%20Resources/payment-session-provider_guxzqt.jpg) *** -## 1. Create a Payment Collection - -A payment collection holds all details related to a resource’s payment operations. So, you start off by creating a payment collection. +## data Property -For example: +Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data. -### Using Workflow +For example, the customer's ID in Stripe is stored in the `data` property. -```ts -import { createPaymentCollectionForCartWorkflow } from "@medusajs/medusa/core-flows" +*** -// ... +## Payment Session Status -await createPaymentCollectionForCartWorkflow(req.scope) - .run({ - input: { - cart_id: "cart_123", - }, - }) -``` +The `status` property of a payment session indicates its current status. Its value can be: -### Using Service +- `pending`: The payment session is awaiting authorization. +- `requires_more`: The payment session requires an action before it’s authorized. For example, to enter a 3DS code. +- `authorized`: The payment session is authorized. +- `error`: An error occurred while authorizing the payment. +- `canceled`: The authorization of the payment session has been canceled. -```ts -const paymentCollection = - await paymentModuleService.createPaymentCollections({ - currency_code: "usd", - amount: 5000, - }) -``` -*** +# Pricing Concepts -## 2. Create Payment Sessions +In this document, you’ll learn about the main concepts in the Pricing Module. -The payment collection has one or more payment sessions, each being a payment amount to be authorized by a payment provider. +## Price Set -So, after creating the payment collection, create at least one payment session for a provider. +A [PriceSet](https://docs.medusajs.com/references/pricing/models/PriceSet/index.html.md) represents a collection of prices that are linked to a resource (for example, a product or a shipping option). -For example: +Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). -### Using Workflow +![A diagram showcasing the relation between the price set and price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648650/Medusa%20Resources/price-set-money-amount_xeees0.jpg) -```ts -import { createPaymentSessionsWorkflow } from "@medusajs/medusa/core-flows" +*** -// ... +## Price List -const { result: paymentSesion } = await createPaymentSessionsWorkflow(req.scope) - .run({ - input: { - payment_collection_id: "paycol_123", - provider_id: "stripe", - }, - }) -``` +A [PriceList](https://docs.medusajs.com/references/pricing/models/PriceList/index.html.md) is a group of prices only enabled if their conditions and rules are satisfied. -### Using Service +A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied. -```ts -const paymentSession = - await paymentModuleService.createPaymentSession( - paymentCollection.id, - { - provider_id: "stripe", - currency_code: "usd", - amount: 5000, - data: { - // any necessary data for the - // payment provider - }, - } - ) -``` +Its associated prices are represented by the `Price` data model. -*** -## 3. Authorize Payment Session +# Prices Calculation -Once the customer chooses a payment session, start the authorization process. This may involve some action performed by the third-party payment provider, such as entering a 3DS code. +In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service. -For example: +## calculatePrices Method -### Using Step +The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts as parameters the ID of one or more price sets and a context. -```ts -import { authorizePaymentSessionStep } from "@medusajs/medusa/core-flows" +It returns a price object with the best matching price for each price set. -// ... +### Calculation Context -authorizePaymentSessionStep({ - id: "payses_123", - context: {}, -}) -``` +The calculation context is an optional object passed as a second parameter to the `calculatePrices` method. It accepts rules to restrict the selected prices in the price set. -### Using Service +For example: ```ts -const payment = authorizePaymentSessionStep({ - id: "payses_123", - context: {}, -}) +const price = await pricingModuleService.calculatePrices( + { id: [priceSetId] }, + { + context: { + currency_code: currencyCode, + region_id: "reg_123", + }, + } +) ``` -When the payment authorization is successful, a payment is created and returned. +In this example, you retrieve the prices in a price set for the specified currency code and region ID. -### Handling Additional Action +### Returned Price Object -If you used the `authorizePaymentSessionStep`, you don't need to implement this logic as it's implemented in the step. +For each price set, the `calculatePrices` method selects two prices: -If the payment authorization isn’t successful, whether because it requires additional action or for another reason, the method updates the payment session with the new status and throws an error. +- A calculated price: Either a price that belongs to a price list and best matches the specified context, or the same as the original price. +- An original price, which is either: + - The same price as the calculated price if the price list it belongs to is of type `override`; + - Or a price that doesn't belong to a price list and best matches the specified context. -In that case, you can catch that error and, if the session's `status` property is `requires_more`, handle the additional action, then retry the authorization. +Both prices are returned in an object that has the following properties: -For example: +- id: (\`string\`) The ID of the price set from which the price was selected. +- is\_calculated\_price\_price\_list: (\`boolean\`) Whether the calculated price belongs to a price list. +- calculated\_amount: (\`number\`) The amount of the calculated price, or \`null\` if there isn't a calculated price. This is the amount shown to the customer. +- is\_original\_price\_price\_list: (\`boolean\`) Whether the original price belongs to a price list. +- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful to compare with the \`calculated\_amount\`, such as to check for discounted value. +- currency\_code: (\`string\`) The currency code of the calculated price, or \`null\` if there isn't a calculated price. +- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) +- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) +- calculated\_price: (\`object\`) The calculated price's price details. -```ts -try { - const payment = - await paymentModuleService.authorizePaymentSession( - paymentSession.id, - {} - ) -} catch (e) { - // retrieve the payment session again - const updatedPaymentSession = ( - await paymentModuleService.listPaymentSessions({ - id: [paymentSession.id], - }) - )[0] + - id: (\`string\`) The ID of the price. - if (updatedPaymentSession.status === "requires_more") { - // TODO perform required action - // TODO authorize payment again. - } -} -``` + - price\_list\_id: (\`string\`) The ID of the associated price list. -*** + - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. -## 4. Payment Flow Complete + - min\_quantity: (\`number\`) The price's min quantity condition. -The payment flow is complete once the payment session is authorized and the payment is created. + - max\_quantity: (\`number\`) The price's max quantity condition. +- original\_price: (\`object\`) The original price's price details. -You can then: + - id: (\`string\`) The ID of the price. -- Capture the payment either using the [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) or [capturePayment method](https://docs.medusajs.com/references/payment/capturePayment/index.html.md). -- Refund captured amounts using the [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) or [refundPayment method](https://docs.medusajs.com/references/payment/refundPayment/index.html.md). + - price\_list\_id: (\`string\`) The ID of the associated price list. -Some payment providers allow capturing the payment automatically once it’s authorized. In that case, you don’t need to do it manually. + - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. + - min\_quantity: (\`number\`) The price's min quantity condition. -# Payment + - max\_quantity: (\`number\`) The price's max quantity condition. -In this document, you’ll learn what a payment is and how it's created, captured, and refunded. +*** -## What's a Payment? +## Examples -When a payment session is authorized, a payment, represented by the [Payment data model](https://docs.medusajs.com/references/payment/models/Payment/index.html.md), is created. This payment can later be captured or refunded. +Consider the following price set: -A payment carries many of the data and relations of a payment session: +```ts +const priceSet = await pricingModuleService.createPriceSets({ + prices: [ + // default price + { + amount: 500, + currency_code: "EUR", + rules: {}, + }, + // prices with rules + { + amount: 400, + currency_code: "EUR", + rules: { + region_id: "reg_123", + }, + }, + { + amount: 450, + currency_code: "EUR", + rules: { + city: "krakow", + }, + }, + { + amount: 500, + currency_code: "EUR", + rules: { + city: "warsaw", + region_id: "reg_123", + }, + }, + ], +}) +``` -- It belongs to the same payment collection. -- It’s associated with the same payment provider, which handles further payment processing. -- It stores the payment session’s `data` property in its `data` property, as it’s still useful for the payment provider’s processing. +### Default Price Selection -*** +### Code -## Capture Payments +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR" + } + } +) +``` -When a payment is captured, a capture, represented by the [Capture data model](https://docs.medusajs.com/references/payment/models/Capture/index.html.md), is created. It holds details related to the capture, such as the amount, the capture date, and more. +### Result -The payment can also be captured incrementally, each time a capture record is created for that amount. +### Calculate Prices with Rules -![A diagram showcasing how a payment's multiple captures are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565445/Medusa%20Resources/payment-capture_f5fve1.jpg) +### Code -*** +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR", + region_id: "reg_123", + city: "krakow" + } + } +) +``` -## Refund Payments +### Result -When a payment is refunded, a refund, represented by the [Refund data model](https://docs.medusajs.com/references/payment/models/Refund/index.html.md), is created. It holds details related to the refund, such as the amount, refund date, and more. +### Price Selection with Price List -A payment can be refunded multiple times, and each time a refund record is created. +### Code -![A diagram showcasing how a payment's multiple refunds are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565555/Medusa%20Resources/payment-refund_lgfvyy.jpg) +```ts +const priceList = pricingModuleService.createPriceLists([{ + title: "Summer Price List", + description: "Price list for summer sale", + starts_at: Date.parse("01/10/2023").toString(), + ends_at: Date.parse("31/10/2023").toString(), + rules: { + region_id: ['PL'] + }, + type: "sale", + prices: [ + { + amount: 400, + currency_code: "EUR", + price_set_id: priceSet.id, + }, + { + amount: 450, + currency_code: "EUR", + price_set_id: priceSet.id, + }, + ], +}]); +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR", + region_id: "PL", + city: "krakow" + } + } +) +``` -# Payment Collection +### Result -In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module. -## What's a Payment Collection? +# Price Rules -A payment collection stores payment details related to a resource, such as a cart or an order. It’s represented by the [PaymentCollection data model](https://docs.medusajs.com/references/payment/models/PaymentCollection/index.html.md). +In this document, you'll learn about price rules for price sets and price lists. -Every purchase or request for payment starts with a payment collection. The collection holds details necessary to complete the payment, including: +## Price Rule -- The [payment sessions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) that represents the payment amount to authorize. -- The [payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md) that are created when a payment session is authorized. They can be captured and refunded. -- The [payment providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) that handle the processing of each payment session, including the authorization, capture, and refund. +You can restrict prices by rules. Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md). -*** +The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price. -## Multiple Payments +For exmaple, you create a price restricted to `10557` zip codes. -The payment collection supports multiple payment sessions and payments. +![A diagram showcasing the relation between the PriceRule and Price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648772/Medusa%20Resources/price-rule-1_vy8bn9.jpg) -You can use this to accept payments in increments or split payments across payment providers. +A price can have multiple price rules. -![Diagram showcasing how a payment collection can have multiple payment sessions and payments](https://res.cloudinary.com/dza7lstvk/image/upload/v1711554695/Medusa%20Resources/payment-collection-multiple-payments_oi3z3n.jpg) +For example, a price can be restricted by a region and a zip code. + +![A diagram showcasing the relation between the PriceRule and Price with multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709649296/Medusa%20Resources/price-rule-3_pwpocz.jpg) *** -## Usage with the Cart Module +## Price List Rules -The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment. +Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md). -During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing. +The `rules_count` property of a `PriceList` indicates how many rules are applied to it. -It also implements the payment flow during checkout as explained in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md). +![A diagram showcasing the relation between the PriceSet, PriceList, Price, RuleType, and PriceListRuleValue](https://res.cloudinary.com/dza7lstvk/image/upload/v1709641999/Medusa%20Resources/price-list_zd10yd.jpg) -![Diagram showcasing the relation between the Payment and Cart modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) +# Tax-Inclusive Pricing -# Payment Module Provider +In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. -In this document, you’ll learn what a payment module provider is. +## What is Tax-Inclusive Pricing? -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage the payment providers available in a region using the dashboard. +A tax-inclusive price is a price of a resource that includes taxes. Medusa calculates the tax amount from the price rather than adds the amount to it. -## What's a Payment Module Provider? +For example, if a product’s price is $50, the tax rate is 2%, and tax-inclusive pricing is enabled, then the product's price is $49, and the applied tax amount is $1. -A payment module provider registers a payment provider that handles payment processing in the Medusa application. It integrates third-party payment providers, such as Stripe. +*** -To authorize a payment amount with a payment provider, a payment session is created and associated with that payment provider. The payment provider is then used to handle the authorization. +## How is Tax-Inclusive Pricing Set? -After the payment session is authorized, the payment provider is associated with the resulting payment and handles its payment processing, such as to capture or refund payment. +The [PricePreference data model](https://docs.medusajs.com/references/pricing/models/PricePreference/index.html.md) holds the tax-inclusive setting for a context. It has two properties that indicate the context: -### List of Payment Module Providers +- `attribute`: The name of the attribute to compare against. For example, `region_id` or `currency_code`. +- `value`: The attribute’s value. For example, `reg_123` or `usd`. -- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md) +Only `region_id` and `currency_code` are supported as an `attribute` at the moment. -*** +The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context. -## System Payment Provider +For example: -The Payment Module provides a `system` payment provider that acts as a placeholder payment provider. +```json +{ + "attribute": "currency_code", + "value": "USD", + "is_tax_inclusive": true, +} +``` -It doesn’t handle payment processing and delegates that to the merchant. It acts similarly to a cash-on-delivery (COD) payment method. +In this example, tax-inclusivity is enabled for the `USD` currency code. *** -## How are Payment Providers Created? +## Tax-Inclusive Pricing in Price Calculation -A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`. +### Tax Context -Refer to [this guide](https://docs.medusajs.com/references/payment/provider/index.html.md) on how to create a payment provider for the Payment Module. +As mentioned in the [Price Calculation documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), The `calculatePrices` method accepts as a parameter a calculation context. -*** +To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context. -## Configure Payment Providers +### Returned Tax Properties -The Payment Module accepts a `providers` option that allows you to register providers in your application. +The `calculatePrices` method returns two properties related to tax-inclusivity: -Learn more about this option in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options#providers/index.html.md). - -*** - -## PaymentProvider Data Model - -When the Medusa application starts and registers the payment providers, it also creates a record of the `PaymentProvider` data model if none exists. - -This data model is used to reference a payment provider and determine whether it’s installed in the application. - - -# Webhook Events - -In this document, you’ll learn how the Payment Module supports listening to webhook events. - -## What's a Webhook Event? - -A webhook event is sent from a third-party payment provider to your application. It indicates a change in a payment’s status. - -This is useful in many cases such as when a payment is being processed asynchronously or when a request is interrupted and the payment provider is sending details on the process later. - -*** - -## getWebhookActionAndData Method - -The Payment Module’s main service has a [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/getWebhookActionAndData/index.html.md) used to handle incoming webhook events from third-party payment services. The method delegates the handling to the associated payment provider, which returns the event's details. - -Medusa implements a webhook listener route at the `/hooks/payment/[identifier]_[provider]` API route, where: - -- `[identifier]` is the `identifier` static property defined in the payment provider. For example, `stripe`. -- `[provider]` is the ID of the provider. For example, `stripe`. - -For example, when integrating basic Stripe payments with the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md), the webhook listener route is `/hooks/payment/stripe_stripe`. If you're integrating Stripe's Bancontact payments, the webhook listener route is `/hooks/payment/stripe-bancontact_stripe`. - -Use that webhook listener in your third-party payment provider's configurations. - -![A diagram showcasing the steps of how the getWebhookActionAndData method words](https://res.cloudinary.com/dza7lstvk/image/upload/v1711567415/Medusa%20Resources/payment-webhook_seaocg.jpg) - -If the event's details indicate that the payment should be authorized, then the [authorizePaymentSession method of the main service](https://docs.medusajs.com/references/payment/authorizePaymentSession/index.html.md) is executed on the specified payment session. - -If the event's details indicate that the payment should be captured, then the [capturePayment method of the main service](https://docs.medusajs.com/references/payment/capturePayment/index.html.md) is executed on the payment of the specified payment session. - -### Actions After Webhook Payment Processing +Learn more about the returned properties in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). -After the payment webhook actions are processed and the payment is authorized or captured, the Medusa application completes the cart associated with the payment's collection if it's not completed yet. +- `is_calculated_price_tax_inclusive`: Whether the selected `calculated_price` is tax-inclusive. +- `is_original_price_tax_inclusive` : Whether the selected `original_price` is tax-inclusive. +A price is considered tax-inclusive if: -# Payment Session +1. It belongs to the region or currency code specified in the calculation context; +2. and the region or currency code has a price preference with `is_tax_inclusive` enabled. -In this document, you’ll learn what a payment session is. +### Tax Context Precedence -## What's a Payment Session? +A region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if: -A payment session, represented by the [PaymentSession data model](https://docs.medusajs.com/references/payment/models/PaymentSession/index.html.md), is a payment amount to be authorized. It’s associated with a payment provider that handles authorizing it. +- both the `region_id` and `currency_code` are provided in the calculation context; +- the selected price belongs to the region; +- and the region has a price preference -A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. -![Diagram showcasing how every payment session has a different payment provider](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565056/Medusa%20Resources/payment-session-provider_guxzqt.jpg) +# Links between Pricing Module and Other Modules -*** +This document showcases the module links defined between the Pricing Module and other commerce modules. -## data Property +## Summary -Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data. +The Pricing Module has the following links to other modules: -For example, the customer's ID in Stripe is stored in the `data` property. +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Stored|| +| in ||Stored|| *** -## Payment Session Status - -The `status` property of a payment session indicates its current status. Its value can be: - -- `pending`: The payment session is awaiting authorization. -- `requires_more`: The payment session requires an action before it’s authorized. For example, to enter a 3DS code. -- `authorized`: The payment session is authorized. -- `error`: An error occurred while authorizing the payment. -- `canceled`: The authorization of the payment session has been canceled. - - -# Links between Region Module and Other Modules - -This document showcases the module links defined between the Region Module and other commerce modules. - -## Summary - -The Region Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -- [`Region` data model \<> `Cart` data model of the Cart Module](#cart-module). (Read-only) -- [`Region` data model \<> `Order` data model of the Order Module](#order-module). (Read-only) -- [`Region` data model \<> `PaymentProvider` data model of the Payment Module](#payment-module). +## Fulfillment Module -*** +The Fulfillment Module provides fulfillment-related functionalities, including shipping options that the customer chooses from when they place their order. However, it doesn't provide pricing-related functionalities for these options. -## Cart Module +Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. -Medusa defines a read-only link between the `Region` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model. This means you can retrieve the details of a region's carts, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. +![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) ### Retrieve with Query -To retrieve the carts of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: +To retrieve the shipping option of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_option.*` in `fields`: ### query.graph ```ts -const { data: regions } = await query.graph({ - entity: "region", +const { data: priceSets } = await query.graph({ + entity: "price_set", fields: [ - "carts.*", + "shipping_option.*", ], }) -// regions.carts +// priceSets.shipping_option ``` ### useQueryGraphStep @@ -24844,81 +24681,84 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: regions } = useQueryGraphStep({ - entity: "region", +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", fields: [ - "carts.*", + "shipping_option.*", ], }) -// regions.carts +// priceSets.shipping_option ``` -*** - -## Order Module +### Manage with Link -Medusa defines a read-only link between the `Region` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model. This means you can retrieve the details of a region's orders, but you don't manage the links in a pivot table in the database. The region of an order is determined by the `region_id` property of the `Order` data model. +To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -### Retrieve with Query +### link.create -To retrieve the orders of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: +```ts +import { Modules } from "@medusajs/framework/utils" -### query.graph +// ... -```ts -const { data: regions } = await query.graph({ - entity: "region", - fields: [ - "orders.*", - ], +await link.create({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, }) - -// regions.orders ``` -### useQueryGraphStep +### createRemoteLinkStep ```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... -const { data: regions } = useQueryGraphStep({ - entity: "region", - fields: [ - "orders.*", - ], +createRemoteLinkStep({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, }) - -// regions.orders ``` *** -## Payment Module +## Product Module -You can specify for each region which payment providers are available for use. +The Product Module doesn't store or manage the prices of product variants. -Medusa defines a module link between the `PaymentProvider` and the `Region` data models. +Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variant’s prices are stored as prices belonging to a price set. -![A diagram showcasing an example of how resources from the Payment and Region modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711569520/Medusa%20Resources/payment-region_jyo2dz.jpg) +![A diagram showcasing an example of how data models from the Pricing and Product Module are linked. The PriceSet is linked to the ProductVariant of the Product Module.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651039/Medusa%20Resources/pricing-product_m4xaut.jpg) + +So, when you want to add prices for a product variant, you create a price set and add the prices to it. + +You can then benefit from adding rules to prices or using the `calculatePrices` method to retrieve the price of a product variant within a specified context. ### Retrieve with Query -To retrieve the payment providers of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_providers.*` in `fields`: +To retrieve the variant of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: ### query.graph ```ts -const { data: regions } = await query.graph({ - entity: "region", +const { data: priceSets } = await query.graph({ + entity: "price_set", fields: [ - "payment_providers.*", + "variant.*", ], }) -// regions.payment_providers +// priceSets.variant ``` ### useQueryGraphStep @@ -24928,19 +24768,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: regions } = useQueryGraphStep({ - entity: "region", +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", fields: [ - "payment_providers.*", + "variant.*", ], }) -// regions.payment_providers +// priceSets.variant ``` ### Manage with Link -To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -24950,11 +24790,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.REGION]: { - region_id: "reg_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` @@ -24968,11 +24808,11 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.REGION]: { - region_id: "reg_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` @@ -25089,48 +24929,228 @@ export interface CampaignBudgetExceededAction { Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties. -# Application Method +# Links between Region Module and Other Modules -In this document, you'll learn what an application method is. +This document showcases the module links defined between the Region Module and other commerce modules. -## What is an Application Method? +## Summary -The [ApplicationMethod data model](https://docs.medusajs.com/references/promotion/models/ApplicationMethod/index.html.md) defines how a promotion is applied: +The Region Module has the following links to other modules: -|Property|Purpose| -|---|---| -|\`type\`|Does the promotion discount a fixed amount or a percentage?| -|\`target\_type\`|Is the promotion applied on a cart item, shipping method, or the entire order?| -|\`allocation\`|Is the discounted amount applied on each item or split between the applicable items?| +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -## Target Promotion Rules +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Read-only|| +| in ||Read-only|| +|| in |Stored|| -When the promotion is applied to a cart item or a shipping method, you can restrict which items/shipping methods the promotion is applied to. +*** -The `ApplicationMethod` data model has a collection of `PromotionRule` records to restrict which items or shipping methods the promotion applies to. The `target_rules` property represents this relation. +## Cart Module -![A diagram showcasing the target\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898273/Medusa%20Resources/application-method-target-rules_hqaymz.jpg) +Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Region` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the region of a cart, and not the other way around. -In this example, the promotion is only applied on products in the cart having the SKU `SHIRT`. +### Retrieve with Query -*** +To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: -## Buy Promotion Rules +### query.graph -When the promotion’s type is `buyget`, you must specify the “buy X” condition. For example, a cart must have two shirts before the promotion can be applied. +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "region.*", + ], +}) -The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation. +// carts.region +``` -![A diagram showcasing the buy\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898453/Medusa%20Resources/application-method-buy-rules_djjuhw.jpg) +### useQueryGraphStep -In this example, the cart must have two products with the SKU `SHIRT` for the promotion to be applied. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +// ... -# Campaign +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "region.*", + ], +}) -In this document, you'll learn about campaigns. +// carts.region +``` -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/campaigns/index.html.md) to learn how to manage campaigns using the dashboard. +*** + +## Order Module + +Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Region` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the region of an order, and not the other way around. + +### Retrieve with Query + +To retrieve the region of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: + +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "region.*", + ], +}) + +// orders.region +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "region.*", + ], +}) + +// orders.region +``` + +*** + +## Payment Module + +You can specify for each region which payment providers are available for use. + +Medusa defines a module link between the `PaymentProvider` and the `Region` data models. + +![A diagram showcasing an example of how resources from the Payment and Region modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711569520/Medusa%20Resources/payment-region_jyo2dz.jpg) + +### Retrieve with Query + +To retrieve the payment providers of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_providers.*` in `fields`: + +### query.graph + +```ts +const { data: regions } = await query.graph({ + entity: "region", + fields: [ + "payment_providers.*", + ], +}) + +// regions.payment_providers +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: regions } = useQueryGraphStep({ + entity: "region", + fields: [ + "payment_providers.*", + ], +}) + +// regions.payment_providers +``` + +### Manage with Link + +To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` + + +# Application Method + +In this document, you'll learn what an application method is. + +## What is an Application Method? + +The [ApplicationMethod data model](https://docs.medusajs.com/references/promotion/models/ApplicationMethod/index.html.md) defines how a promotion is applied: + +|Property|Purpose| +|---|---| +|\`type\`|Does the promotion discount a fixed amount or a percentage?| +|\`target\_type\`|Is the promotion applied on a cart item, shipping method, or the entire order?| +|\`allocation\`|Is the discounted amount applied on each item or split between the applicable items?| + +## Target Promotion Rules + +When the promotion is applied to a cart item or a shipping method, you can restrict which items/shipping methods the promotion is applied to. + +The `ApplicationMethod` data model has a collection of `PromotionRule` records to restrict which items or shipping methods the promotion applies to. The `target_rules` property represents this relation. + +![A diagram showcasing the target\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898273/Medusa%20Resources/application-method-target-rules_hqaymz.jpg) + +In this example, the promotion is only applied on products in the cart having the SKU `SHIRT`. + +*** + +## Buy Promotion Rules + +When the promotion’s type is `buyget`, you must specify the “buy X” condition. For example, a cart must have two shirts before the promotion can be applied. + +The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation. + +![A diagram showcasing the buy\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898453/Medusa%20Resources/application-method-buy-rules_djjuhw.jpg) + +In this example, the cart must have two products with the SKU `SHIRT` for the promotion to be applied. + + +# Campaign + +In this document, you'll learn about campaigns. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/campaigns/index.html.md) to learn how to manage campaigns using the dashboard. ## What is a Campaign? @@ -25219,9 +25239,11 @@ The Promotion Module has the following links to other modules: Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -- [`Cart` data model of the Cart Module \<> `Promotion` data model](#cart-module). -- [`LineItemAdjustment` data model of the Cart Module \<> `Promotion` data model](#cart-module). (Read-only). -- [`Order` data model of the Order Module \<> `Promotion` data model](#order-module). +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Stored|| +| in ||Read-only|| +| in ||Stored|| *** @@ -25231,13 +25253,13 @@ A promotion can be applied on line items and shipping methods of a cart. Medusa ![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) -Medusa also defines a read-only link between the `Promotion` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItemAdjustment` data model. This means you can retrieve the details of the promotion applied on a line item, but you don't manage the links in a pivot table in the database. The promotion of a line item is determined by the `promotion_id` property of the `LineItemAdjustment` data model. +Medusa also defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItemAdjustment` data model and the `Promotion` data model. Because the link is read-only from the `LineItemAdjustment`'s side, you can only retrieve the promotion applied on a line item, and not the other way around. ### Retrieve with Query To retrieve the carts that a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: -To retrieve the line item adjustments of a promotion, pass `line_item_adjustments.*` in `fields`. +To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. ### query.graph @@ -25400,12 +25422,14 @@ The Product Module has the following links to other modules: Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -- [`Product` data model \<> `Cart` data model of Cart Module](#cart-module). (Read-only). -- [`Product` data model \<> `ShippingProfile` data model of Fulfillment Module](#fulfillment-module). -- [`ProductVariant` data model \<> `InventoryItem` data model of Inventory Module](#inventory-module). -- [`Product` data model \<> `Order` data model of Order Module](#order-module). (Read-only). -- [`ProductVariant` data model \<> `PriceSet` data model of Pricing Module](#pricing-module). -- [`Product` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Read-only|| +|| in |Stored|| +|| in |Stored|| +| in ||Read-only|| +|| in |Stored|| +|| in |Stored|| *** @@ -25413,26 +25437,26 @@ Read-only links are used to query data across modules, but the relations aren't Medusa defines read-only links between: -- The `Product` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `LineItem` data model. -- The `ProductVariant` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `LineItem` data model. +- The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model and the `Product` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the product of a line item, and not the other way around. +- The `ProductVariant` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the variant of a line item, and not the other way around. ### Retrieve with Query -To retrieve the line items of a variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `line_items.*` in `fields`: +To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: -To retrieve the line items of a product, pass `line_items.*` in `fields`. +To retrieve the product, pass `product.*` in `fields`. ### query.graph ```ts -const { data: variants } = await query.graph({ - entity: "variant", +const { data: lineItems } = await query.graph({ + entity: "line_item", fields: [ - "line_items.*", + "variant.*", ], }) -// variants.line_items +// lineItems.variant ``` ### useQueryGraphStep @@ -25442,14 +25466,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: variants } = useQueryGraphStep({ - entity: "variant", +const { data: lineItems } = useQueryGraphStep({ + entity: "line_item", fields: [ - "line_items.*", + "variant.*", ], }) -// variants.line_items +// lineItems.variant ``` *** @@ -25626,26 +25650,26 @@ createRemoteLinkStep({ Medusa defines read-only links between: -- the `Product` data model and the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `OrderLineItem` data model. -- the `ProductVariant` data model and the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `OrderLineItem` data model. +- the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model and the `Product` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the product of an order line item, and not the other way around. +- the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model and the `ProductVariant` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the variant of an order line item, and not the other way around. ### Retrieve with Query -To retrieve the order line items of a variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order_items.*` in `fields`: +To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: -To retrieve a product's order line items, pass `order_items.*` in `fields`. +To retrieve the product, pass `product.*` in `fields`. ### query.graph ```ts -const { data: variants } = await query.graph({ - entity: "variant", +const { data: lineItems } = await query.graph({ + entity: "order_line_item", fields: [ - "order_items.*", + "variant.*", ], }) -// variants.order_items +// lineItems.variant ``` ### useQueryGraphStep @@ -25655,14 +25679,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: variants } = useQueryGraphStep({ - entity: "variant", +const { data: lineItems } = useQueryGraphStep({ + entity: "order_line_item", fields: [ - "order_items.*", + "variant.*", ], }) -// variants.order_items +// lineItems.variant ``` *** @@ -25951,6 +25975,30 @@ By combining configurations of shipment requirements and inventory management, y |Item that doesn't require shipping and its variant inventory isn't managed by Medusa.||| +# Publishable API Keys with Sales Channels + +In this document, you’ll learn what publishable API keys are and how to use them with sales channels. + +## Publishable API Keys with Sales Channels + +A publishable API key, provided by the API Key Module, is a client key scoped to one or more sales channels. + +When sending a request to a Store API route, you must pass a publishable API key in the header of the request: + +```bash +curl http://localhost:9000/store/products \ + x-publishable-api-key: {your_publishable_api_key} +``` + +The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used. + +*** + +## How to Create a Publishable API Key? + +To create a publishable API key, either use the [Medusa Admin](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys/index.html.md) or the [Admin API Routes](https://docs.medusajs.com/api/admin#publishable-api-keys). + + # Stock Location Concepts In this document, you’ll learn about the main concepts in the Stock Location Module. @@ -25968,52 +26016,49 @@ Medusa uses stock locations to provide inventory details, from the Inventory Mod The `StockLocationAddress` data model belongs to the `StockLocation` data model. It provides more detailed information of the location, such as country code or street address. -# Links between Stock Location Module and Other Modules +# Links between Sales Channel Module and Other Modules -This document showcases the module links defined between the Stock Location Module and other commerce modules. +This document showcases the module links defined between the Sales Channel Module and other commerce modules. ## Summary -The Stock Location Module has the following links to other modules: +The Sales Channel Module has the following links to other modules: Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -- [`FulfillmentSet` data model of the Fulfillment Module \<> `StockLocation` data model](#fulfillment-module). -- [`FulfillmentProvider` data model of the Fulfillment Module \<> `StockLocation` data model](#fulfillment-module). -- [`StockLocation` data model \<> `Inventory` data model of the Inventory Module](#inventory-module). -- [`SalesChannel` data model of the Sales Channel Module \<> `StockLocation` data model](#sales-channel-module). +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Stored|| +| in ||Read-only|| +| in ||Read-only|| +| in ||Stored|| +|| in |Stored|| *** -## Fulfillment Module - -A fulfillment set can be conditioned to a specific stock location. - -Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. +## API Key Module -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) +A publishable API key allows you to easily specify the sales channel scope in a client request. -Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. +Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg) +![A diagram showcasing an example of how resources from the Sales Channel and API Key modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) ### Retrieve with Query -To retrieve the fulfillment sets of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillment_sets.*` in `fields`: - -To retrieve the fulfillment providers, pass `fulfillment_providers.*` in `fields`. +To retrieve the API keys associated with a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `publishable_api_keys.*` in `fields`: ### query.graph ```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", fields: [ - "fulfillment_sets.*", + "publishable_api_keys.*", ], }) -// stockLocations.fulfillment_sets +// salesChannels.publishable_api_keys ``` ### useQueryGraphStep @@ -26023,237 +26068,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", fields: [ - "fulfillment_sets.*", + "publishable_api_keys.*", ], }) -// stockLocations.fulfillment_sets -``` - -### Manage with Link - -To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` - -*** - -## Inventory Module - -Medusa defines a read-only link between the `StockLocation` data model and the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md)'s `InventoryLevel` data model. This means you can retrieve the details of a stock location's inventory levels, but you don't manage the links in a pivot table in the database. The stock location of an inventory level is determined by the `location_id` property of the `InventoryLevel` data model. - -### Retrieve with Query - -To retrieve the inventory levels of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `inventory_levels.*` in `fields`: - -### query.graph - -```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: [ - "inventory_levels.*", - ], -}) - -// stockLocations.inventory_levels -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: [ - "inventory_levels.*", - ], -}) - -// stockLocations.inventory_levels -``` - -*** - -## Sales Channel Module - -A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel. - -Medusa defines a link between the `SalesChannel` and `StockLocation` data models. - -![A diagram showcasing an example of how resources from the Sales Channel and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716796872/Medusa%20Resources/sales-channel-location_cqrih1.jpg) - -### Retrieve with Query - -To retrieve the sales channels of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: - -### query.graph - -```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: [ - "sales_channels.*", - ], -}) - -// stockLocations.sales_channels -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: [ - "sales_channels.*", - ], -}) - -// stockLocations.sales_channels -``` - -### Manage with Link - -To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` - - -# Links between Sales Channel Module and Other Modules - -This document showcases the module links defined between the Sales Channel Module and other commerce modules. - -## Summary - -The Sales Channel Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -- [`ApiKey` data model of the API Key Module \<> `SalesChannel` data model](#api-key-module). -- [`SalesChannel` data model \<> `Cart` data model of the Cart Module](#cart-module). (Read-only) -- [`SalesChannel` data model \<> `Order` data model of the Order Module](#order-module). (Read-only) -- [`Product` data model of the Product Module \<> `SalesChannel` data model](#product-module). -- [`SalesChannel` data model \<> `StockLocation` data model of the Stock Location Module](#stock-location-module). - -*** - -## API Key Module - -A publishable API key allows you to easily specify the sales channel scope in a client request. - -Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. - -![A diagram showcasing an example of how resources from the Sales Channel and API Key modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) - -### Retrieve with Query - -To retrieve the API keys associated with a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `publishable_api_keys.*` in `fields`: - -### query.graph - -```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "publishable_api_keys.*", - ], -}) - -// salesChannels.publishable_api_keys -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "publishable_api_keys.*", - ], -}) - -// salesChannels.publishable_api_keys +// salesChannels.publishable_api_keys ``` ### Manage with Link @@ -26299,23 +26121,23 @@ createRemoteLinkStep({ ## Cart Module -Medusa defines a read-only link between the `SalesChannel` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model. This means you can retrieve the details of a sales channel's carts, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. +Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `SalesChannel` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the sales channel of a cart, and not the other way around. ### Retrieve with Query -To retrieve the carts of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: +To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: ### query.graph ```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", +const { data: carts } = await query.graph({ + entity: "cart", fields: [ - "carts.*", + "sales_channel.*", ], }) -// salesChannels.carts +// carts.sales_channel ``` ### useQueryGraphStep @@ -26325,37 +26147,37 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", +const { data: carts } = useQueryGraphStep({ + entity: "cart", fields: [ - "carts.*", + "sales_channel.*", ], }) -// salesChannels.carts +// carts.sales_channel ``` *** ## Order Module -Medusa defines a read-only link between the `SalesChannel` data model and the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model. This means you can retrieve the details of a sales channel's orders, but you don't manage the links in a pivot table in the database. The sales channel of an order is determined by the `sales_channel_id` property of the `Order` data model. +Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `SalesChannel` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the sales channel of an order, and not the other way around. ### Retrieve with Query -To retrieve the orders of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: +To retrieve the sales channel of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: ### query.graph ```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", +const { data: orders } = await query.graph({ + entity: "order", fields: [ - "orders.*", + "sales_channel.*", ], }) -// salesChannels.orders +// orders.sales_channel ``` ### useQueryGraphStep @@ -26365,14 +26187,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ - "orders.*", + "sales_channel.*", ], }) -// salesChannels.orders +// orders.sales_channel ``` *** @@ -26542,28 +26364,234 @@ createRemoteLinkStep({ ``` -# Publishable API Keys with Sales Channels +# Links between Stock Location Module and Other Modules -In this document, you’ll learn what publishable API keys are and how to use them with sales channels. +This document showcases the module links defined between the Stock Location Module and other commerce modules. -## Publishable API Keys with Sales Channels +## Summary -A publishable API key, provided by the API Key Module, is a client key scoped to one or more sales channels. +The Stock Location Module has the following links to other modules: -When sending a request to a Store API route, you must pass a publishable API key in the header of the request: +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -```bash -curl http://localhost:9000/store/products \ - x-publishable-api-key: {your_publishable_api_key} -``` +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Stored|| +| in ||Stored|| +| in ||Stored|| +| in ||Stored|| -The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used. +*** + +## Fulfillment Module + +A fulfillment set can be conditioned to a specific stock location. + +Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. + +![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) + +Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. + +![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg) + +### Retrieve with Query + +To retrieve the fulfillment sets of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillment_sets.*` in `fields`: + +To retrieve the fulfillment providers, pass `fulfillment_providers.*` in `fields`. + +### query.graph + +```ts +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: [ + "fulfillment_sets.*", + ], +}) + +// stockLocations.fulfillment_sets +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: [ + "fulfillment_sets.*", + ], +}) + +// stockLocations.fulfillment_sets +``` + +### Manage with Link + +To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` *** -## How to Create a Publishable API Key? +## Inventory Module -To create a publishable API key, either use the [Medusa Admin](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys/index.html.md) or the [Admin API Routes](https://docs.medusajs.com/api/admin#publishable-api-keys). +Medusa defines a read-only link between the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md)'s `InventoryLevel` data model and the `StockLocation` data model. Because the link is read-only from the `InventoryLevel`'s side, you can only retrieve the stock location of an inventory level, and not the other way around. + +### Retrieve with Query + +To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: + +### query.graph + +```ts +const { data: inventoryLevels } = await query.graph({ + entity: "inventory_level", + fields: [ + "stock_locations.*", + ], +}) + +// inventoryLevels.stock_locations +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: inventoryLevels } = useQueryGraphStep({ + entity: "inventory_level", + fields: [ + "stock_locations.*", + ], +}) + +// inventoryLevels.stock_locations +``` + +*** + +## Sales Channel Module + +A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel. + +Medusa defines a link between the `SalesChannel` and `StockLocation` data models. + +![A diagram showcasing an example of how resources from the Sales Channel and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716796872/Medusa%20Resources/sales-channel-location_cqrih1.jpg) + +### Retrieve with Query + +To retrieve the sales channels of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: + +### query.graph + +```ts +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: [ + "sales_channels.*", + ], +}) + +// stockLocations.sales_channels +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: [ + "sales_channels.*", + ], +}) + +// stockLocations.sales_channels +``` + +### Manage with Link + +To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, +}) +``` # Links between Store Module and Other Modules @@ -26576,7 +26604,9 @@ The Store Module has the following links to other modules: Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -- [`Currency` data model \<> `Currency` data model of Currency Module](#currency-module). (Read-only). +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|| in |Read-only|| *** @@ -26584,7 +26614,7 @@ Read-only links are used to query data across modules, but the relations aren't The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. -Instead, Medusa defines a read-only link between the [Currency Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md)'s `Currency` data model and the Store Module's `Currency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database. The currencies of a store are determined by the `currency_code` of the `Currency` data model in the Store Module. +Instead, Medusa defines a read-only link between the [Currency Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md)'s `Currency` data model and the Store Module's `Currency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database. The currencies of a store are determined by the `currency_code` of the [Currency](https://docs.medusajs.com/references/store/models/Currency/index.html.md) data model in the Store Module (not in the Currency Module). ### Retrieve with Query @@ -26738,44 +26768,6 @@ JWT_SECRET=supersecret ``` -# Tax Rates and Rules - -In this document, you’ll learn about tax rates and rules. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions#manage-tax-rate-overrides/index.html.md) to learn how to manage tax rates using the dashboard. - -## What are Tax Rates? - -A tax rate is a percentage amount used to calculate the tax amount for each taxable item’s price, such as line items or shipping methods, in a cart. The sum of all calculated tax amounts are then added to the cart’s total as a tax total. - -Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. - -### Combinable Tax Rates - -Tax regions can have parent tax regions. To inherit the tax rates of the parent tax region, set the `is_combinable` of the child’s tax rates to `true`. - -Then, when tax rates are retrieved for a taxable item in the child region, both the child and the parent tax regions’ applicable rates are returned. - -*** - -## Override Tax Rates with Rules - -You can create tax rates that override the default for specific conditions or rules. - -For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15. - -A tax region can have multiple tax rates, and each tax rate can have multiple tax rules. The [TaxRateRule data model](https://docs.medusajs.com/references/tax/models/TaxRateRule/index.html.md) represents a tax rate’s rule. - -![A diagram showcasing the relation between TaxRegion, TaxRate, and TaxRateRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1711462167/Medusa%20Resources/tax-rate-rule_enzbp2.jpg) - -These two properties of the data model identify the rule’s target: - -- `reference`: the name of the table in the database that this rule points to. For example, `product_type`. -- `reference_id`: the ID of the data model’s record that this points to. For example, a product type’s ID. - -So, to override the default tax rate for product types “Shirt”, you create a tax rate and associate with it a tax rule whose `reference` is `product_type` and `reference_id` the ID of the “Shirt” product type. - - # Tax Calculation with the Tax Provider In this document, you’ll learn how tax lines are calculated and what a tax provider is. @@ -26896,101 +26888,57 @@ The objects in the array accept the following properties: - `options`: An optional object of the module provider's options. -# Tax Region +# Tax Rates and Rules -In this document, you’ll learn about tax regions and how to use them with the Region Module. +In this document, you’ll learn about tax rates and rules. -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions#manage-tax-rate-overrides/index.html.md) to learn how to manage tax rates using the dashboard. -## What is a Tax Region? +## What are Tax Rates? -A tax region, represented by the [TaxRegion data model](https://docs.medusajs.com/references/tax/models/TaxRegion/index.html.md), stores tax settings related to a region that your store serves. +A tax rate is a percentage amount used to calculate the tax amount for each taxable item’s price, such as line items or shipping methods, in a cart. The sum of all calculated tax amounts are then added to the cart’s total as a tax total. -Tax regions can inherit settings and rules from a parent tax region. - -Each tax region has tax rules and a tax provider. - - -# GitHub Auth Module Provider +Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. -In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module. +### Combinable Tax Rates -The Github Auth Module Provider authenticates users with their GitHub account. +Tax regions can have parent tax regions. To inherit the tax rates of the parent tax region, set the `is_combinable` of the child’s tax rates to `true`. -Learn about the authentication flow in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). +Then, when tax rates are retrieved for a taxable item in the child region, both the child and the parent tax regions’ applicable rates are returned. *** -## Register the Github Auth Module Provider - -### Prerequisites - -- [Register GitHub App. When setting the Callback URL, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://docs.github.com/en/apps/creating-github-apps/setting-up-a-github-app/creating-a-github-app) -- [Retrieve the client ID and client secret of your GitHub App](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28#using-basic-authentication) - -Add the module to the array of providers passed to the Auth Module: +## Override Tax Rates with Rules -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" +You can create tax rates that override the default for specific conditions or rules. -// ... +For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15. -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/auth", - dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], - options: { - providers: [ - // other providers... - { - resolve: "@medusajs/medusa/auth-github", - id: "github", - options: { - clientId: process.env.GITHUB_CLIENT_ID, - clientSecret: process.env.GITHUB_CLIENT_SECRET, - callbackUrl: process.env.GITHUB_CALLBACK_URL, - }, - }, - ], - }, - }, - ], -}) -``` +A tax region can have multiple tax rates, and each tax rate can have multiple tax rules. The [TaxRateRule data model](https://docs.medusajs.com/references/tax/models/TaxRateRule/index.html.md) represents a tax rate’s rule. -### Environment Variables +![A diagram showcasing the relation between TaxRegion, TaxRate, and TaxRateRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1711462167/Medusa%20Resources/tax-rate-rule_enzbp2.jpg) -Make sure to add the necessary environment variables for the above options in `.env`: +These two properties of the data model identify the rule’s target: -```plain -GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= -GITHUB_CALLBACK_URL= -``` +- `reference`: the name of the table in the database that this rule points to. For example, `product_type`. +- `reference_id`: the ID of the data model’s record that this points to. For example, a product type’s ID. -### Module Options +So, to override the default tax rate for product types “Shirt”, you create a tax rate and associate with it a tax rule whose `reference` is `product_type` and `reference_id` the ID of the “Shirt” product type. -|Configuration|Description|Required| -|---|---|---|---|---| -|\`clientId\`|A string indicating the client ID of your GitHub app.|Yes| -|\`clientSecret\`|A string indicating the client secret of your GitHub app.|Yes| -|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in GitHub.|Yes| -*** +# Tax Region -## Override Callback URL During Authentication +In this document, you’ll learn about tax regions and how to use them with the Region Module. -In many cases, you may have different callback URL for actor types. For example, you may redirect admin users to a different URL than customers after authentication. +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. -The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md) can accept a `callback_url` body parameter to override the provider's `callbackUrl` option. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md). +## What is a Tax Region? -*** +A tax region, represented by the [TaxRegion data model](https://docs.medusajs.com/references/tax/models/TaxRegion/index.html.md), stores tax settings related to a region that your store serves. -## Examples +Tax regions can inherit settings and rules from a parent tax region. -- [How to implement third-party / social login in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). +Each tax region has tax rules and a tax provider. # Emailpass Auth Module Provider @@ -27142,6 +27090,88 @@ The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednass - [How to implement Google social login in the storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). +# GitHub Auth Module Provider + +In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module. + +The Github Auth Module Provider authenticates users with their GitHub account. + +Learn about the authentication flow in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). + +*** + +## Register the Github Auth Module Provider + +### Prerequisites + +- [Register GitHub App. When setting the Callback URL, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://docs.github.com/en/apps/creating-github-apps/setting-up-a-github-app/creating-a-github-app) +- [Retrieve the client ID and client secret of your GitHub App](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28#using-basic-authentication) + +Add the module to the array of providers passed to the Auth Module: + +```ts title="medusa-config.ts" +import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/auth", + dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], + options: { + providers: [ + // other providers... + { + resolve: "@medusajs/medusa/auth-github", + id: "github", + options: { + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackUrl: process.env.GITHUB_CALLBACK_URL, + }, + }, + ], + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the necessary environment variables for the above options in `.env`: + +```plain +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GITHUB_CALLBACK_URL= +``` + +### Module Options + +|Configuration|Description|Required| +|---|---|---|---|---| +|\`clientId\`|A string indicating the client ID of your GitHub app.|Yes| +|\`clientSecret\`|A string indicating the client secret of your GitHub app.|Yes| +|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in GitHub.|Yes| + +*** + +## Override Callback URL During Authentication + +In many cases, you may have different callback URL for actor types. For example, you may redirect admin users to a different URL than customers after authentication. + +The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md) can accept a `callback_url` body parameter to override the provider's `callbackUrl` option. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md). + +*** + +## Examples + +- [How to implement third-party / social login in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). + + # Stripe Module Provider In this document, you’ll learn about the Stripe Module Provider and how to configure it in the Payment Module. @@ -27519,549 +27549,549 @@ For each product variant, you: ## Workflows -- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) - [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md) - [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) -- [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md) - [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) +- [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md) - [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/index.html.md) - [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) - [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md) - [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md) -- [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md) - [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md) - [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md) +- [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md) - [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) - [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md) +- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) - [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md) - [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md) - [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md) -- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) - [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md) - [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/index.html.md) +- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) - [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/index.html.md) -- [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) - [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/index.html.md) +- [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) - [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/index.html.md) - [updateApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateApiKeysWorkflow/index.html.md) -- [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/index.html.md) -- [dismissLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissLinksWorkflow/index.html.md) -- [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/index.html.md) - [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/index.html.md) +- [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/index.html.md) +- [dismissLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissLinksWorkflow/index.html.md) +- [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/index.html.md) +- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md) +- [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md) +- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md) +- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md) - [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) - [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md) +- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) - [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md) - [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md) -- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md) - [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) -- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) +- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md) - [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md) -- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md) -- [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md) -- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md) -- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md) -- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) -- [calculateShippingOptionsPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/calculateShippingOptionsPricesWorkflow/index.html.md) +- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) +- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md) - [batchShippingOptionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchShippingOptionRulesWorkflow/index.html.md) +- [calculateShippingOptionsPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/calculateShippingOptionsPricesWorkflow/index.html.md) - [cancelFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelFulfillmentWorkflow/index.html.md) - [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md) -- [createReturnFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnFulfillmentWorkflow/index.html.md) - [createServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createServiceZonesWorkflow/index.html.md) -- [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md) -- [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/index.html.md) - [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/index.html.md) +- [createReturnFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnFulfillmentWorkflow/index.html.md) +- [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/index.html.md) - [deleteServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteServiceZonesWorkflow/index.html.md) - [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md) +- [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md) - [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/index.html.md) -- [markFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markFulfillmentAsDeliveredWorkflow/index.html.md) - [updateFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateFulfillmentWorkflow/index.html.md) - [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md) +- [markFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markFulfillmentAsDeliveredWorkflow/index.html.md) - [updateShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionsWorkflow/index.html.md) - [updateShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingProfilesWorkflow/index.html.md) - [validateFulfillmentDeliverabilityStep](https://docs.medusajs.com/references/medusa-workflows/validateFulfillmentDeliverabilityStep/index.html.md) - [batchInventoryItemLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchInventoryItemLevelsWorkflow/index.html.md) - [bulkCreateDeleteLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/bulkCreateDeleteLevelsWorkflow/index.html.md) +- [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md) - [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md) +- [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md) - [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md) -- [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md) - [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md) -- [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md) - [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md) - [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/index.html.md) -- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md) -- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md) - [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md) -- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) -- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md) -- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md) +- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md) - [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) +- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md) +- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md) +- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) - [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md) -- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md) -- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md) -- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) -- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md) -- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/index.html.md) -- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) -- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md) - [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) - [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) -- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) - [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/index.html.md) -- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/index.html.md) - [validatePaymentsRefundStep](https://docs.medusajs.com/references/medusa-workflows/validatePaymentsRefundStep/index.html.md) +- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) +- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/index.html.md) +- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) +- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md) +- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) +- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) +- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md) - [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/index.html.md) - [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/index.html.md) -- [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md) - [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md) -- [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/index.html.md) - [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md) -- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md) -- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md) +- [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/index.html.md) +- [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md) - [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md) - [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md) +- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md) +- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md) - [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md) -- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md) -- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md) - [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md) -- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md) +- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md) - [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md) +- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md) - [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md) -- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md) - [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md) +- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md) +- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md) - [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) - [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md) - [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md) -- [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md) - [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md) +- [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md) - [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md) -- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) - [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md) - [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/index.html.md) -- [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md) +- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) - [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md) +- [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md) - [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/index.html.md) - [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/index.html.md) - [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/index.html.md) - [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/index.html.md) -- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md) - [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md) +- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md) - [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md) - [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) - [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md) -- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md) -- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md) - [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) +- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md) - [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md) +- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md) - [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md) - [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) -- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) -- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md) - [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md) +- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md) +- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) - [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) - [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md) +- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md) - [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md) - [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md) -- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md) - [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md) +- [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/index.html.md) - [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/index.html.md) - [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md) -- [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/index.html.md) -- [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md) -- [createOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodValidationStep/index.html.md) - [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/index.html.md) +- [createOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodValidationStep/index.html.md) +- [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md) +- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md) - [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) - [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md) - [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md) -- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md) - [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md) - [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md) -- [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md) - [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md) +- [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md) - [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/index.html.md) -- [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/index.html.md) - [declineTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/declineTransferOrderRequestValidationStep/index.html.md) +- [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/index.html.md) +- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md) +- [deleteOrderPaymentCollections](https://docs.medusajs.com/references/medusa-workflows/deleteOrderPaymentCollections/index.html.md) - [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/index.html.md) - [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md) -- [deleteOrderPaymentCollections](https://docs.medusajs.com/references/medusa-workflows/deleteOrderPaymentCollections/index.html.md) -- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md) - [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) -- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) -- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) - [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md) +- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) +- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) - [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md) - [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md) -- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) - [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md) - [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md) +- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) - [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md) -- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/index.html.md) -- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md) -- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md) - [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md) -- [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/index.html.md) +- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md) +- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md) - [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md) +- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/index.html.md) +- [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/index.html.md) - [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md) - [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md) -- [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md) - [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md) - [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md) +- [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md) +- [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) - [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md) -- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md) - [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) -- [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) +- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md) +- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md) - [removeClaimAddItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimAddItemActionValidationStep/index.html.md) -- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md) - [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md) -- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md) -- [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md) -- [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md) +- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md) - [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md) - [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md) -- [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md) +- [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md) +- [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md) - [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md) - [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md) -- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md) +- [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md) - [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md) - [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md) - [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md) - [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md) +- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md) - [removeReturnItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnItemActionValidationStep/index.html.md) - [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md) -- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md) - [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/index.html.md) - [requestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnWorkflow/index.html.md) +- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md) - [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/index.html.md) - [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md) - [requestOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferValidationStep/index.html.md) -- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md) - [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md) - [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md) -- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md) +- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md) - [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md) - [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) +- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md) - [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md) +- [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/index.html.md) - [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md) - [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/index.html.md) - [updateExchangeAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemWorkflow/index.html.md) -- [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/index.html.md) -- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md) -- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md) - [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md) -- [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md) - [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md) -- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) +- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md) +- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md) +- [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md) - [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) -- [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md) +- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) - [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md) - [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md) +- [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md) - [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md) -- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) - [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md) -- [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md) -- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) - [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/index.html.md) +- [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md) +- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) - [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) -- [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md) +- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) - [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/index.html.md) -- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md) +- [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md) - [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md) -- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) -- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md) -- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) -- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md) -- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) +- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md) +- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md) +- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md) +- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) +- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) +- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/index.html.md) +- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md) +- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md) - [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/index.html.md) - [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/index.html.md) - [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md) -- [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/index.html.md) -- [batchPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPromotionRulesWorkflow/index.html.md) -- [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md) -- [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md) -- [createCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCampaignsWorkflow/index.html.md) -- [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md) -- [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/index.html.md) -- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md) -- [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/index.html.md) -- [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/index.html.md) -- [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/index.html.md) -- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md) -- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md) -- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md) - [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md) -- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) - [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) -- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md) +- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) +- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md) - [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md) -- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md) - [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md) +- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md) - [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) -- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md) -- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) +- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md) - [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) +- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) +- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md) - [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) -- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) - [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md) -- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) -- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md) - [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md) +- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) +- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md) +- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) - [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md) - [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md) -- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) - [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md) +- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) - [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md) - [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) - [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md) +- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) - [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md) - [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) -- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) - [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) +- [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md) +- [deleteReservationsByLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsByLineItemsWorkflow/index.html.md) +- [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/index.html.md) +- [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/index.html.md) - [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md) -- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md) - [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/index.html.md) -- [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/index.html.md) +- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md) +- [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/index.html.md) +- [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md) +- [createCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCampaignsWorkflow/index.html.md) +- [batchPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPromotionRulesWorkflow/index.html.md) +- [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md) +- [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md) +- [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/index.html.md) +- [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/index.html.md) +- [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/index.html.md) +- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md) +- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md) +- [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/index.html.md) +- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md) - [createReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnReasonsWorkflow/index.html.md) +- [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/index.html.md) - [updateReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnReasonsWorkflow/index.html.md) -- [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md) -- [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/index.html.md) -- [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/index.html.md) -- [deleteReservationsByLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsByLineItemsWorkflow/index.html.md) -- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) - [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md) +- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) - [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md) - [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md) -- [createStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStoresWorkflow/index.html.md) -- [updateStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStoresWorkflow/index.html.md) -- [deleteStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStoresWorkflow/index.html.md) - [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md) - [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/index.html.md) -- [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md) -- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md) -- [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md) -- [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/index.html.md) -- [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/index.html.md) +- [createStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStoresWorkflow/index.html.md) +- [deleteStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStoresWorkflow/index.html.md) +- [updateStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStoresWorkflow/index.html.md) +- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) - [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md) - [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md) +- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md) - [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md) -- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) - [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md) -- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md) - [maybeListTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/maybeListTaxRateRuleIdsStep/index.html.md) -- [setTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/setTaxRateRulesWorkflow/index.html.md) - [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md) - [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/index.html.md) +- [setTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/setTaxRateRulesWorkflow/index.html.md) - [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) - [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md) -- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) - [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md) - [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md) +- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) +- [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md) +- [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/index.html.md) +- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md) +- [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md) +- [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/index.html.md) ## Steps -- [deleteApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteApiKeysStep/index.html.md) +- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) - [createApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/createApiKeysStep/index.html.md) - [linkSalesChannelsToApiKeyStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkSalesChannelsToApiKeyStep/index.html.md) +- [deleteApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteApiKeysStep/index.html.md) - [revokeApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/revokeApiKeysStep/index.html.md) - [updateApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateApiKeysStep/index.html.md) - [validateSalesChannelsExistStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateSalesChannelsExistStep/index.html.md) -- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) -- [dismissRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/dismissRemoteLinkStep/index.html.md) -- [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md) -- [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/index.html.md) -- [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/index.html.md) -- [updateRemoteLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRemoteLinksStep/index.html.md) -- [emitEventStep](https://docs.medusajs.com/references/medusa-workflows/steps/emitEventStep/index.html.md) -- [validatePresenceOfStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePresenceOfStep/index.html.md) -- [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md) -- [createCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerAddressesStep/index.html.md) -- [createCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomersStep/index.html.md) -- [deleteCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerAddressesStep/index.html.md) -- [deleteCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomersStep/index.html.md) -- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md) -- [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md) -- [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/index.html.md) -- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md) -- [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md) -- [createCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerGroupsStep/index.html.md) -- [deleteCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerGroupStep/index.html.md) -- [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/index.html.md) -- [updateCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerGroupsStep/index.html.md) -- [linkCustomersToCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomersToCustomerGroupStep/index.html.md) -- [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/index.html.md) - [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/index.html.md) -- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md) -- [createPaymentCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentCollectionsStep/index.html.md) - [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/index.html.md) +- [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/index.html.md) +- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md) - [createLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemsStep/index.html.md) +- [createPaymentCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentCollectionsStep/index.html.md) - [createShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingMethodAdjustmentsStep/index.html.md) - [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/index.html.md) -- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/index.html.md) -- [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/index.html.md) - [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md) +- [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/index.html.md) - [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/index.html.md) - [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md) +- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/index.html.md) - [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md) - [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md) - [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) - [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md) - [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) -- [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md) - [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md) +- [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md) - [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md) -- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md) - [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md) +- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md) - [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md) - [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/index.html.md) - [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md) - [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md) - [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md) -- [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md) - [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md) -- [validateShippingStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingStep/index.html.md) +- [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md) - [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md) - [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/index.html.md) +- [validateShippingStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingStep/index.html.md) - [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md) +- [createCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerAddressesStep/index.html.md) +- [deleteCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerAddressesStep/index.html.md) +- [createCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomersStep/index.html.md) +- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md) +- [deleteCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomersStep/index.html.md) +- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md) +- [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md) +- [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/index.html.md) +- [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md) +- [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md) +- [dismissRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/dismissRemoteLinkStep/index.html.md) +- [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/index.html.md) +- [emitEventStep](https://docs.medusajs.com/references/medusa-workflows/steps/emitEventStep/index.html.md) +- [updateRemoteLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRemoteLinksStep/index.html.md) +- [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/index.html.md) +- [validatePresenceOfStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePresenceOfStep/index.html.md) +- [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md) +- [createCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerGroupsStep/index.html.md) +- [deleteCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerGroupStep/index.html.md) +- [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/index.html.md) +- [linkCustomersToCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomersToCustomerGroupStep/index.html.md) +- [updateCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerGroupsStep/index.html.md) - [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md) - [deleteFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFilesStep/index.html.md) - [uploadFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/uploadFilesStep/index.html.md) - [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/index.html.md) - [calculateShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/calculateShippingOptionsPricesStep/index.html.md) -- [cancelFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelFulfillmentStep/index.html.md) - [createFulfillmentSets](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentSets/index.html.md) +- [cancelFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelFulfillmentStep/index.html.md) - [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md) - [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md) - [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md) +- [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md) - [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/index.html.md) - [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md) -- [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md) - [deleteFulfillmentSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFulfillmentSetsStep/index.html.md) - [deleteServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteServiceZonesStep/index.html.md) -- [setShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/setShippingOptionsPricesStep/index.html.md) - [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md) - [deleteShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionsStep/index.html.md) -- [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md) -- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md) +- [setShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/setShippingOptionsPricesStep/index.html.md) - [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/index.html.md) +- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md) +- [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md) +- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md) - [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/index.html.md) - [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/index.html.md) -- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md) - [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/index.html.md) -- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md) -- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md) -- [validateTokenStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTokenStep/index.html.md) -- [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md) -- [notifyOnFailureStep](https://docs.medusajs.com/references/medusa-workflows/steps/notifyOnFailureStep/index.html.md) -- [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) - [adjustInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/adjustInventoryLevelsStep/index.html.md) -- [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/index.html.md) - [attachInventoryItemToVariants](https://docs.medusajs.com/references/medusa-workflows/steps/attachInventoryItemToVariants/index.html.md) +- [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/index.html.md) - [createInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryLevelsStep/index.html.md) - [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/index.html.md) -- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md) +- [deleteInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryLevelsStep/index.html.md) - [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/index.html.md) +- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md) - [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md) -- [deleteInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryLevelsStep/index.html.md) - [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md) - [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md) - [deleteLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteLineItemsStep/index.html.md) - [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/index.html.md) - [updateLineItemsStepWithSelector](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStepWithSelector/index.html.md) +- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md) +- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md) +- [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md) +- [validateTokenStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTokenStep/index.html.md) +- [notifyOnFailureStep](https://docs.medusajs.com/references/medusa-workflows/steps/notifyOnFailureStep/index.html.md) +- [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) +- [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/index.html.md) +- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md) +- [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md) +- [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md) +- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md) - [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/index.html.md) - [addOrderTransactionStep](https://docs.medusajs.com/references/medusa-workflows/steps/addOrderTransactionStep/index.html.md) - [cancelOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderChangeStep/index.html.md) - [cancelOrderClaimStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderClaimStep/index.html.md) - [cancelOrderExchangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderExchangeStep/index.html.md) - [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md) +- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md) - [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/index.html.md) - [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md) - [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/index.html.md) -- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md) - [createOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderChangeStep/index.html.md) - [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/index.html.md) -- [createOrderExchangeItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangeItemsFromActionsStep/index.html.md) - [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/index.html.md) -- [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md) +- [createOrderExchangeItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangeItemsFromActionsStep/index.html.md) - [createOrderExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangesStep/index.html.md) - [createOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrdersStep/index.html.md) +- [declineOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/declineOrderChangeStep/index.html.md) +- [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md) - [createReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnsStep/index.html.md) - [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/index.html.md) -- [declineOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/declineOrderChangeStep/index.html.md) - [deleteExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteExchangesStep/index.html.md) - [deleteOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangeActionsStep/index.html.md) - [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/index.html.md) +- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md) - [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md) - [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md) -- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md) - [previewOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/previewOrderChangeStep/index.html.md) - [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md) - [registerOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderFulfillmentStep/index.html.md) -- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md) -- [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/index.html.md) - [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/index.html.md) - [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md) +- [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/index.html.md) +- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md) - [updateOrderShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsStep/index.html.md) - [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) -- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md) - [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/index.html.md) -- [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/index.html.md) -- [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md) -- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md) -- [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md) -- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md) +- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md) - [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/index.html.md) -- [createPaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentSessionStep/index.html.md) - [createRefundReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRefundReasonStep/index.html.md) +- [createPaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentSessionStep/index.html.md) - [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/index.html.md) - [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/index.html.md) -- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md) - [validateDeletedPaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDeletedPaymentSessionsStep/index.html.md) - [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md) -- [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/index.html.md) +- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md) - [createPriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListPricesStep/index.html.md) -- [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/index.html.md) +- [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/index.html.md) - [deletePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePriceListsStep/index.html.md) - [removePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/removePriceListPricesStep/index.html.md) - [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md) -- [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md) - [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/index.html.md) +- [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/index.html.md) - [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md) +- [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md) +- [createProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductCategoriesStep/index.html.md) +- [deleteProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductCategoriesStep/index.html.md) +- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/index.html.md) - [createPricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPricePreferencesStep/index.html.md) +- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/index.html.md) - [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md) - [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/index.html.md) -- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/index.html.md) -- [updatePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesStep/index.html.md) - [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/index.html.md) +- [updatePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesStep/index.html.md) - [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md) - [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/index.html.md) -- [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/index.html.md) -- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md) - [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/index.html.md) +- [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/index.html.md) - [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/index.html.md) +- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md) +- [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md) - [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md) +- [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md) - [deleteCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCollectionsStep/index.html.md) -- [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md) - [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/index.html.md) -- [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md) - [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/index.html.md) - [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md) - [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md) -- [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/index.html.md) - [generateProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/generateProductCsvStep/index.html.md) - [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md) +- [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/index.html.md) - [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md) +- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/index.html.md) - [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md) - [groupProductsForBatchStep](https://docs.medusajs.com/references/medusa-workflows/steps/groupProductsForBatchStep/index.html.md) -- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/index.html.md) - [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md) - [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/index.html.md) - [updateProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTagsStep/index.html.md) - [updateProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTypesStep/index.html.md) -- [updateProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductVariantsStep/index.html.md) - [updateProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductsStep/index.html.md) - [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/index.html.md) -- [createProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductCategoriesStep/index.html.md) -- [deleteProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductCategoriesStep/index.html.md) -- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/index.html.md) +- [updateProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductVariantsStep/index.html.md) - [addCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addCampaignPromotionsStep/index.html.md) - [addRulesToPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addRulesToPromotionsStep/index.html.md) - [createCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCampaignsStep/index.html.md) @@ -28078,42 +28108,42 @@ For each product variant, you: - [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/index.html.md) - [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md) - [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md) -- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md) - [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md) - [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md) -- [deleteReturnReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnReasonStep/index.html.md) +- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md) - [createReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnReasonsStep/index.html.md) - [updateReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnReasonsStep/index.html.md) -- [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/index.html.md) -- [deleteShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingProfilesStep/index.html.md) +- [deleteReturnReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnReasonStep/index.html.md) - [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md) +- [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/index.html.md) - [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/index.html.md) - [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md) -- [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/index.html.md) - [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/index.html.md) - [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/index.html.md) -- [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/index.html.md) - [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/index.html.md) - [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md) +- [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/index.html.md) +- [deleteShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingProfilesStep/index.html.md) +- [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/index.html.md) - [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/index.html.md) -- [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/index.html.md) - [updateStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStockLocationsStep/index.html.md) -- [createStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/createStoresStep/index.html.md) -- [updateStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStoresStep/index.html.md) +- [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/index.html.md) - [deleteStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStoresStep/index.html.md) +- [updateStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStoresStep/index.html.md) +- [createStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/createStoresStep/index.html.md) +- [createUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createUsersStep/index.html.md) - [deleteUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteUsersStep/index.html.md) - [updateUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateUsersStep/index.html.md) -- [createUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createUsersStep/index.html.md) -- [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md) +- [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/index.html.md) - [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md) +- [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md) - [deleteTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRateRulesStep/index.html.md) -- [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/index.html.md) - [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md) - [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md) - [listTaxRateIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateIdsStep/index.html.md) - [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/index.html.md) -- [listTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateRuleIdsStep/index.html.md) - [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/index.html.md) +- [listTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateRuleIdsStep/index.html.md) - [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/index.html.md) @@ -28140,129 +28170,6 @@ npx medusa --help *** -# build Command - Medusa CLI Reference - -Create a standalone build of the Medusa application. - -This creates a build that: - -- Doesn't rely on the source TypeScript files. -- Can be copied to a production server reliably. - -The build is outputted to a new `.medusa/server` directory. - -```bash -npx medusa build -``` - -Refer to [this section](#run-built-medusa-application) for next steps. - -## Options - -|Option|Description| -|---|---|---| -|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the | - -*** - -## Run Built Medusa Application - -After running the `build` command, use the following step to run the built Medusa application: - -- Change to the `.medusa/server` directory and install the dependencies: - -```bash npm2yarn -cd .medusa/server && npm install -``` - -- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead. - -```bash npm2yarn -cp .env .medusa/server/.env.production -``` - -- In the system environment variables, set `NODE_ENV` to `production`: - -```bash -NODE_ENV=production -``` - -- Use the `start` command to run the application: - -```bash npm2yarn -cd .medusa/server && npm run start -``` - -*** - -## Build Medusa Admin - -By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory. - -If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead. - - -# develop Command - Medusa CLI Reference - -Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. - -```bash -npx medusa develop -``` - -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| - - -# new Command - Medusa CLI Reference - -Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project. - -```bash -medusa new [ []] -``` - -## Arguments - -|Argument|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-| -|\`starter\_url\`|The name of the directory to create the Medusa application in.|No|\`https://github.com/medusajs/medusa-starter-default\`| - -## Options - -|Option|Description| -|---|---|---| -|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.| -|\`--skip-db\`|Skip database creation.| -|\`--skip-env\`|Skip populating | -|\`--db-user \\`|The database user to use for database setup.| -|\`--db-database \\`|The name of the database used for database setup.| -|\`--db-pass \\`|The database password to use for database setup.| -|\`--db-port \\`|The database port to use for database setup.| -|\`--db-host \\`|The database host to use for database setup.| - - -# exec Command - Medusa CLI Reference - -Run a custom CLI script. Learn more about it in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md). - -```bash -npx medusa exec [file] [args...] -``` - -## Arguments - -|Argument|Description|Required| -|---|---|---|---|---| -|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes| -|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| - - # db Commands - Medusa CLI Reference Commands starting with `db:` perform actions on the database. @@ -28383,25 +28290,148 @@ npx medusa db:sync-links |\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| -# plugin Commands - Medusa CLI Reference +# build Command - Medusa CLI Reference -Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development. +Create a standalone build of the Medusa application. -These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). +This creates a build that: -## plugin:publish +- Doesn't rely on the source TypeScript files. +- Can be copied to a production server reliably. -Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command. +The build is outputted to a new `.medusa/server` directory. ```bash -npx medusa plugin:publish +npx medusa build ``` -*** - -## plugin:add +Refer to [this section](#run-built-medusa-application) for next steps. -Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command. +## Options + +|Option|Description| +|---|---|---| +|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the | + +*** + +## Run Built Medusa Application + +After running the `build` command, use the following step to run the built Medusa application: + +- Change to the `.medusa/server` directory and install the dependencies: + +```bash npm2yarn +cd .medusa/server && npm install +``` + +- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead. + +```bash npm2yarn +cp .env .medusa/server/.env.production +``` + +- In the system environment variables, set `NODE_ENV` to `production`: + +```bash +NODE_ENV=production +``` + +- Use the `start` command to run the application: + +```bash npm2yarn +cd .medusa/server && npm run start +``` + +*** + +## Build Medusa Admin + +By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory. + +If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead. + + +# develop Command - Medusa CLI Reference + +Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. + +```bash +npx medusa develop +``` + +## Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`-H \\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \\`|Set port of the Medusa server.|\`9000\`| + + +# new Command - Medusa CLI Reference + +Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project. + +```bash +medusa new [ []] +``` + +## Arguments + +|Argument|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-| +|\`starter\_url\`|The name of the directory to create the Medusa application in.|No|\`https://github.com/medusajs/medusa-starter-default\`| + +## Options + +|Option|Description| +|---|---|---| +|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.| +|\`--skip-db\`|Skip database creation.| +|\`--skip-env\`|Skip populating | +|\`--db-user \\`|The database user to use for database setup.| +|\`--db-database \\`|The name of the database used for database setup.| +|\`--db-pass \\`|The database password to use for database setup.| +|\`--db-port \\`|The database port to use for database setup.| +|\`--db-host \\`|The database host to use for database setup.| + + +# start Command - Medusa CLI Reference + +Start the Medusa application in production. + +```bash +npx medusa start +``` + +## Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`-H \\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \\`|Set port of the Medusa server.|\`9000\`| + + +# plugin Commands - Medusa CLI Reference + +Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development. + +These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + +## plugin:publish + +Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command. + +```bash +npx medusa plugin:publish +``` + +*** + +## plugin:add + +Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command. ```bash npx medusa plugin:add [names...] @@ -28444,39 +28474,39 @@ npx medusa plugin:build ``` -# start Command - Medusa CLI Reference +# start-cluster Command - Medusa CLI Reference -Start the Medusa application in production. +Starts the Medusa application in [cluster mode](https://expressjs.com/en/advanced/best-practice-performance.html#run-your-app-in-a-cluster). + +Running in cluster mode significantly improves performance as the workload and tasks are distributed among all available instances instead of a single one. ```bash -npx medusa start +npx medusa start-cluster ``` -## Options +#### Options |Option|Description|Default| |---|---|---|---|---| +|\`-c \\`|The number of CPUs that Medusa can consume.|Medusa will try to consume all CPUs.| |\`-H \\`|Set host of the Medusa server.|\`localhost\`| |\`-p \\`|Set port of the Medusa server.|\`9000\`| -# start-cluster Command - Medusa CLI Reference - -Starts the Medusa application in [cluster mode](https://expressjs.com/en/advanced/best-practice-performance.html#run-your-app-in-a-cluster). +# telemetry Command - Medusa CLI Reference -Running in cluster mode significantly improves performance as the workload and tasks are distributed among all available instances instead of a single one. +Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. ```bash -npx medusa start-cluster +npx medusa telemetry ``` #### Options -|Option|Description|Default| -|---|---|---|---|---| -|\`-c \\`|The number of CPUs that Medusa can consume.|Medusa will try to consume all CPUs.| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| +|Option|Description| +|---|---|---| +|\`--enable\`|Enable telemetry (default).| +|\`--disable\`|Disable telemetry.| # user Command - Medusa CLI Reference @@ -28498,20 +28528,20 @@ npx medusa user --email [--password ] If ran successfully, you'll receive the invite token in the output.|No|\`false\`| -# telemetry Command - Medusa CLI Reference +# exec Command - Medusa CLI Reference -Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. +Run a custom CLI script. Learn more about it in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md). ```bash -npx medusa telemetry +npx medusa exec [file] [args...] ``` -#### Options +## Arguments -|Option|Description| -|---|---|---| -|\`--enable\`|Enable telemetry (default).| -|\`--disable\`|Disable telemetry.| +|Argument|Description|Required| +|---|---|---|---|---| +|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes| +|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| # Medusa CLI Reference @@ -28615,22 +28645,6 @@ npx medusa develop |\`-p \\`|Set port of the Medusa server.|\`9000\`| -# exec Command - Medusa CLI Reference - -Run a custom CLI script. Learn more about it in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md). - -```bash -npx medusa exec [file] [args...] -``` - -## Arguments - -|Argument|Description|Required| -|---|---|---|---|---| -|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes| -|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| - - # db Commands - Medusa CLI Reference Commands starting with `db:` perform actions on the database. @@ -28751,6 +28765,22 @@ npx medusa db:sync-links |\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| +# exec Command - Medusa CLI Reference + +Run a custom CLI script. Learn more about it in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md). + +```bash +npx medusa exec [file] [args...] +``` + +## Arguments + +|Argument|Description|Required| +|---|---|---|---|---| +|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes| +|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| + + # new Command - Medusa CLI Reference Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project. @@ -28857,6 +28887,22 @@ npx medusa start |\`-p \\`|Set port of the Medusa server.|\`9000\`| +# telemetry Command - Medusa CLI Reference + +Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. + +```bash +npx medusa telemetry +``` + +#### Options + +|Option|Description| +|---|---|---| +|\`--enable\`|Enable telemetry (default).| +|\`--disable\`|Disable telemetry.| + + # user Command - Medusa CLI Reference Create a new admin user. @@ -28895,22 +28941,6 @@ npx medusa start-cluster |\`-p \\`|Set port of the Medusa server.|\`9000\`| -# telemetry Command - Medusa CLI Reference - -Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. - -```bash -npx medusa telemetry -``` - -#### Options - -|Option|Description| -|---|---|---| -|\`--enable\`|Enable telemetry (default).| -|\`--disable\`|Disable telemetry.| - - # Medusa JS SDK In this documentation, you'll learn how to install and use Medusa's JS SDK. @@ -29211,301 +29241,301 @@ The object or class passed to `auth.storage` configuration must have the followi ## JS SDK Admin -- [create](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.create/index.html.md) -- [batchSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.batchSalesChannels/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.delete/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md) -- [revoke](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.revoke/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.delete/index.html.md) - [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.update/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.list/index.html.md) -- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.retrieve/index.html.md) - [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md) -- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md) +- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md) - [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/index.html.md) -- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md) +- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md) - [cancel](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancel/index.html.md) - [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md) -- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md) +- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md) - [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/index.html.md) -- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeInboundItem/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md) -- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/index.html.md) +- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md) +- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeInboundItem/index.html.md) - [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/index.html.md) +- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/index.html.md) - [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md) -- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md) -- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/index.html.md) - [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md) +- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md) +- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md) - [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundItem/index.html.md) - [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundShipping/index.html.md) +- [batchSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.batchSalesChannels/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md) +- [revoke](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.revoke/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.retrieve/index.html.md) - [clearToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken/index.html.md) -- [clearToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken_/index.html.md) - [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md) - [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md) -- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md) +- [clearToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken_/index.html.md) - [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md) +- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md) - [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/index.html.md) - [getTokenStorageInfo\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getTokenStorageInfo_/index.html.md) -- [setToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken/index.html.md) - [getToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken_/index.html.md) -- [setToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken_/index.html.md) - [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/index.html.md) +- [setToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken/index.html.md) +- [setToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken_/index.html.md) - [throwError\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.throwError_/index.html.md) -- [getItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.getItem/index.html.md) -- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md) -- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md) -- [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.delete/index.html.md) +- [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.create/index.html.md) - [batchCustomerGroups](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.batchCustomerGroups/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.delete/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieve/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md) -- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.createShipment/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md) +- [getItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.getItem/index.html.md) +- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md) +- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/index.html.md) - [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/index.html.md) -- [listFulfillmentOptions](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.listFulfillmentOptions/index.html.md) - [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md) -- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md) - [cancel](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancel/index.html.md) - [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/index.html.md) -- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md) +- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md) - [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/index.html.md) -- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md) - [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md) +- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/index.html.md) - [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md) - [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/index.html.md) -- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md) - [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md) -- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md) +- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md) - [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md) +- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md) +- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.createShipment/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.create/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.update/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.list/index.html.md) +- [listFulfillmentOptions](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.listFulfillmentOptions/index.html.md) - [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md) -- [retrieveServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.retrieveServiceZone/index.html.md) - [deleteServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.deleteServiceZone/index.html.md) +- [retrieveServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.retrieveServiceZone/index.html.md) - [updateServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.updateServiceZone/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.retrieve/index.html.md) - [batchInventoryItemLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemLocationLevels/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.create/index.html.md) -- [batchUpdateLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchUpdateLevels/index.html.md) - [batchInventoryItemsLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemsLocationLevels/index.html.md) +- [batchUpdateLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchUpdateLevels/index.html.md) +- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md) -- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md) - [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md) - [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md) -- [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md) -- [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancel/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.list/index.html.md) - [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/index.html.md) -- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md) - [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancel/index.html.md) - [createFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createFulfillment/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.list/index.html.md) +- [listChanges](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listChanges/index.html.md) +- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md) - [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md) - [markAsDelivered](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.markAsDelivered/index.html.md) -- [listChanges](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listChanges/index.html.md) - [requestTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.requestTransfer/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrieve/index.html.md) - [retrievePreview](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrievePreview/index.html.md) -- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md) +- [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.update/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md) +- [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/index.html.md) +- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md) - [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.cancelRequest/index.html.md) - [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/index.html.md) - [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md) - [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/index.html.md) +- [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md) - [updateOriginalItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateOriginalItem/index.html.md) - [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md) -- [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/index.html.md) -- [markAsPaid](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.markAsPaid/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md) -- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/index.html.md) - [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md) +- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/index.html.md) - [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.create/index.html.md) - [batchPrices](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.batchPrices/index.html.md) - [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.create/index.html.md) +- [markAsPaid](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.markAsPaid/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.list/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md) -- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md) - [batchVariantInventoryItems](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantInventoryItems/index.html.md) +- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md) +- [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md) - [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/index.html.md) +- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md) +- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.create/index.html.md) -- [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md) -- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md) -- [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/index.html.md) -- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md) +- [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md) - [export](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.export/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md) -- [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/index.html.md) +- [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/index.html.md) - [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/index.html.md) -- [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md) +- [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/index.html.md) - [listVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listVariants/index.html.md) -- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md) - [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md) +- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md) -- [updateVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateVariant/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md) - [updateOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateOption/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/index.html.md) -- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md) +- [updateVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateVariant/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.retrieve/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.update/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.retrieve/index.html.md) - [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.updateProducts/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.delete/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.update/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.update/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.update/index.html.md) - [addRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.addRules/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.delete/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.delete/index.html.md) - [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/index.html.md) -- [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md) - [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md) - [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/index.html.md) +- [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.create/index.html.md) - [updateRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.updateRules/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.create/index.html.md) +- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.update/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.retrieve/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.delete/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.update/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.delete/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.create/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.update/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.retrieve/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/index.html.md) -- [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.create/index.html.md) +- [batchProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.batchProducts/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.update/index.html.md) - [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md) -- [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md) +- [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md) - [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md) +- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.updateProducts/index.html.md) - [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelRequest/index.html.md) +- [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md) - [confirmReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmReceive/index.html.md) - [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md) -- [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/index.html.md) -- [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/index.html.md) - [dismissItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.dismissItems/index.html.md) +- [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/index.html.md) +- [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/index.html.md) - [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/index.html.md) -- [removeDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeDismissItem/index.html.md) -- [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md) - [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md) +- [removeDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeDismissItem/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.retrieve/index.html.md) - [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md) +- [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md) - [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md) +- [updateReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnItem/index.html.md) - [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.retrieve/index.html.md) - [updateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateRequest/index.html.md) -- [updateReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnItem/index.html.md) - [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/index.html.md) -- [batchProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.batchProducts/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.update/index.html.md) -- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.updateProducts/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.retrieve/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.create/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/index.html.md) +- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.retrieve/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.create/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md) -- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.update/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md) - [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md) - [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md) - [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.update/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.retrieve/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.retrieve/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.retrieve/index.html.md) - [me](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.me/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.update/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.retrieve/index.html.md) @@ -29514,22 +29544,22 @@ The object or class passed to `auth.storage` configuration must have the followi - [callback](https://docs.medusajs.com/references/js-sdk/auth/callback/index.html.md) - [login](https://docs.medusajs.com/references/js-sdk/auth/login/index.html.md) -- [logout](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md) -- [register](https://docs.medusajs.com/references/js-sdk/auth/register/index.html.md) - [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/index.html.md) -- [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md) - [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md) +- [register](https://docs.medusajs.com/references/js-sdk/auth/register/index.html.md) +- [logout](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md) +- [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md) ## JS SDK Store - [cart](https://docs.medusajs.com/references/js-sdk/store/cart/index.html.md) - [category](https://docs.medusajs.com/references/js-sdk/store/category/index.html.md) -- [collection](https://docs.medusajs.com/references/js-sdk/store/collection/index.html.md) -- [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md) - [customer](https://docs.medusajs.com/references/js-sdk/store/customer/index.html.md) -- [payment](https://docs.medusajs.com/references/js-sdk/store/payment/index.html.md) +- [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md) +- [collection](https://docs.medusajs.com/references/js-sdk/store/collection/index.html.md) - [order](https://docs.medusajs.com/references/js-sdk/store/order/index.html.md) +- [payment](https://docs.medusajs.com/references/js-sdk/store/payment/index.html.md) - [product](https://docs.medusajs.com/references/js-sdk/store/product/index.html.md) - [region](https://docs.medusajs.com/references/js-sdk/store/region/index.html.md) @@ -30259,143 +30289,388 @@ export default CustomPage This UI route also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and a [Header]() custom components. -# Container - Admin Components +# Two Column Layout - Admin Components -The Medusa Admin wraps each section of a page in a container. +The Medusa Admin has pages with two columns of content. -![Example of a container in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728287102/Medusa%20Resources/container_soenir.png) +This doesn't include the sidebar, only the main content. -To create a component that uses the same container styling in your widgets or UI routes, create the file `src/admin/components/container.tsx` with the following content: +![An example of an admin page with two columns](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286690/Medusa%20Resources/two-column_sdnkg0.png) -```tsx -import { - Container as UiContainer, - clx, -} from "@medusajs/ui" +To create a layout that you can use in UI routes to support two columns of content, create the component `src/admin/layouts/two-column.tsx` with the following content: -type ContainerProps = React.ComponentProps +```tsx title="src/admin/layouts/two-column.tsx" +export type TwoColumnLayoutProps = { + firstCol: React.ReactNode + secondCol: React.ReactNode +} -export const Container = (props: ContainerProps) => { +export const TwoColumnLayout = ({ + firstCol, + secondCol, +}: TwoColumnLayoutProps) => { return ( - +
+
+ {firstCol} +
+
+ {secondCol} +
+
) } ``` -The `Container` component re-uses the component from the [Medusa UI package](https://docs.medusajs.com/ui/components/container/index.html.md) and applies to it classes to match the Medusa Admin's design conventions. +The `TwoColumnLayout` accepts two props: + +- `firstCol` indicating the content of the first column. +- `secondCol` indicating the content of the second column. *** ## Example -Use that `Container` component in any widget or UI route. - -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: +Use the `TwoColumnLayout` component in your UI routes that have a single column. For example: -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container } from "../components/container" -import { Header } from "../components/header" +```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]} +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" +import { Container } from "../../components/container" +import { Header } from "../../components/header" +import { TwoColumnLayout } from "../../layouts/two-column" -const ProductWidget = () => { +const CustomPage = () => { return ( - -
- + +
+ + } + secondCol={ + +
+ + } + /> ) } -export const config = defineWidgetConfig({ - zone: "product.details.before", +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, }) -export default ProductWidget +export default CustomPage ``` -This widget also uses a [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. - - -# Data Table - Admin Components - -This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). - -The [DataTable component in Medusa UI](https://docs.medusajs.com/ui/components/data-table/index.html.md) allows you to display data in a table with sorting, filtering, and pagination. It's used across the Medusa Admin dashboard to showcase a list of items, such as a list of products. - -![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png) +This UI route also uses [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header]() custom components. -You can use this component in your Admin Extensions to display data in a table format, especially if you're retrieving them from API routes of the Medusa application. -This guide focuses on how to use the `DataTable` component while fetching data from the backend. Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/components/data-table/index.html.md) for detailed information about the DataTable component and its different usages. +# Action Menu - Admin Components -## Example: DataTable with Data Fetching +The Medusa Admin often provides additional actions in a dropdown shown when users click a three-dot icon. -In this example, you'll create a UI widget that shows the list of products retrieved from the [List Products API Route](https://docs.medusajs.com/api/admin#products_getproducts) in a data table with pagination, filtering, searching, and sorting. +![Example of an action menu in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728291319/Medusa%20Resources/action-menu_jnus6k.png) -Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI: +To create a component that shows this menu in your customizations, create the file `src/admin/components/action-menu.tsx` with the following content: -```tsx title="src/admin/routes/custom/page.tsx" -import { - createDataTableColumnHelper, -} from "@medusajs/ui" +```tsx title="src/admin/components/action-menu.tsx" import { - HttpTypes, -} from "@medusajs/framework/types" - -const columnHelper = createDataTableColumnHelper() - -const columns = [ - columnHelper.accessor("title", { - header: "Title", - // Enables sorting for the column. - enableSorting: true, - // If omitted, the header will be used instead if it's a string, - // otherwise the accessor key (id) will be used. - sortLabel: "Title", - // If omitted the default value will be "A-Z" - sortAscLabel: "A-Z", - // If omitted the default value will be "Z-A" - sortDescLabel: "Z-A", - }), - columnHelper.accessor("status", { - header: "Status", - cell: ({ getValue }) => { - const status = getValue() - return ( - - {status === "published" ? "Published" : "Draft"} - - ) - }, - }), -] -``` + DropdownMenu, + IconButton, + clx, +} from "@medusajs/ui" +import { EllipsisHorizontal } from "@medusajs/icons" +import { Link } from "react-router-dom" -`createDataTableColumnHelper` utility creates a column helper that helps you define the columns for the data table. The column helper has an `accessor` method that accepts two parameters: +export type Action = { + icon: React.ReactNode + label: string + disabled?: boolean +} & ( + | { + to: string + onClick?: never + } + | { + onClick: () => void + to?: never + } +) -1. The column's key in the table's data. -2. An object with the following properties: - - `header`: The column's header. - - `cell`: (optional) By default, a data's value for a column is displayed as a string. Use this property to specify custom rendering of the value. It accepts a function that returns a string or a React node. The function receives an object that has a `getValue` property function to retrieve the raw value of the cell. - - `enableSorting`: (optional) A boolean that enables sorting data by this column. - - `sortLabel`: (optional) The label for the sorting button. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used. - - `sortAscLabel`: (optional) The label for the ascending sorting button. If omitted, the default value will be "A-Z". - - `sortDescLabel`: (optional) The label for the descending sorting button. If omitted, the default value will be "Z-A". +export type ActionGroup = { + actions: Action[] +} -Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status. +export type ActionMenuProps = { + groups: ActionGroup[] +} -To define the filters, add the following: +export const ActionMenu = ({ groups }: ActionMenuProps) => { + return ( + + + + + + + + {groups.map((group, index) => { + if (!group.actions.length) { + return null + } -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { - // ... - createDataTableFilterHelper, -} from "@medusajs/ui" + const isLast = index === groups.length - 1 -const filterHelper = createDataTableFilterHelper() + return ( + + {group.actions.map((action, index) => { + if (action.onClick) { + return ( + { + e.stopPropagation() + action.onClick() + }} + className={clx( + "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2", + { + "[&_svg]:text-ui-fg-disabled": action.disabled, + } + )} + > + {action.icon} + {action.label} + + ) + } + + return ( +
+ + e.stopPropagation()}> + {action.icon} + {action.label} + + +
+ ) + })} + {!isLast && } +
+ ) + })} +
+
+ ) +} +``` + +The `ActionMenu` component shows a three-dots icon (or `EllipsisHorizontal`) from the [Medusa Icons package](https://docs.medusajs.com/ui/icons/overview/index.html.md) in a button. + +When the button is clicked, a dropdown menu is shown with the actions passed in the props. + +The component accepts the following props: + +- groups: (\`object\[]\`) Groups of actions to be shown in the dropdown. Each group is separated by a divider. + + - actions: (\`object\[]\`) Actions in the group. + + - icon: (\`React.ReactNode\`) + + - label: (\`string\`) The action's text. + + - disabled: (\`boolean\`) Whether the action is shown as disabled. + + - \`to\`: (\`string\`) The link to take the user to when they click the action. This is required if \`onClick\` isn't provided. + + - \`onClick\`: (\`() => void\`) The function to execute when the action is clicked. This is required if \`to\` isn't provided. + +*** + +## Example + +Use the `ActionMenu` component in any widget or UI route. + +For example, create the widget `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 { Pencil } from "@medusajs/icons" +import { Container } from "../components/container" +import { ActionMenu } from "../components/action-menu" + +const ProductWidget = () => { + return ( + + , + label: "Edit", + onClick: () => { + alert("You clicked the edit action!") + }, + }, + ], + }, + ]} /> + + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component. + +### Use in Header + +You can also use the action menu in the [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) component as part of its actions. + +For example: + +```tsx title="src/admin/widgets/product-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Pencil } from "@medusajs/icons" +import { Container } from "../components/container" +import { Header } from "../components/header" + +const ProductWidget = () => { + return ( + +
, + label: "Edit", + onClick: () => { + alert("You clicked the edit action!") + }, + }, + ], + }, + ], + }, + }, + ]} + /> + + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + + +# Data Table - Admin Components + +This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). + +The [DataTable component in Medusa UI](https://docs.medusajs.com/ui/components/data-table/index.html.md) allows you to display data in a table with sorting, filtering, and pagination. It's used across the Medusa Admin dashboard to showcase a list of items, such as a list of products. + +![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png) + +You can use this component in your Admin Extensions to display data in a table format, especially if you're retrieving them from API routes of the Medusa application. + +This guide focuses on how to use the `DataTable` component while fetching data from the backend. Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/components/data-table/index.html.md) for detailed information about the DataTable component and its different usages. + +## Example: DataTable with Data Fetching + +In this example, you'll create a UI widget that shows the list of products retrieved from the [List Products API Route](https://docs.medusajs.com/api/admin#products_getproducts) in a data table with pagination, filtering, searching, and sorting. + +Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI: + +```tsx title="src/admin/routes/custom/page.tsx" +import { + createDataTableColumnHelper, +} from "@medusajs/ui" +import { + HttpTypes, +} from "@medusajs/framework/types" + +const columnHelper = createDataTableColumnHelper() + +const columns = [ + columnHelper.accessor("title", { + header: "Title", + // Enables sorting for the column. + enableSorting: true, + // If omitted, the header will be used instead if it's a string, + // otherwise the accessor key (id) will be used. + sortLabel: "Title", + // If omitted the default value will be "A-Z" + sortAscLabel: "A-Z", + // If omitted the default value will be "Z-A" + sortDescLabel: "Z-A", + }), + columnHelper.accessor("status", { + header: "Status", + cell: ({ getValue }) => { + const status = getValue() + return ( + + {status === "published" ? "Published" : "Draft"} + + ) + }, + }), +] +``` + +`createDataTableColumnHelper` utility creates a column helper that helps you define the columns for the data table. The column helper has an `accessor` method that accepts two parameters: + +1. The column's key in the table's data. +2. An object with the following properties: + - `header`: The column's header. + - `cell`: (optional) By default, a data's value for a column is displayed as a string. Use this property to specify custom rendering of the value. It accepts a function that returns a string or a React node. The function receives an object that has a `getValue` property function to retrieve the raw value of the cell. + - `enableSorting`: (optional) A boolean that enables sorting data by this column. + - `sortLabel`: (optional) The label for the sorting button. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used. + - `sortAscLabel`: (optional) The label for the ascending sorting button. If omitted, the default value will be "A-Z". + - `sortDescLabel`: (optional) The label for the descending sorting button. If omitted, the default value will be "Z-A". + +Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status. + +To define the filters, add the following: + +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { + // ... + createDataTableFilterHelper, +} from "@medusajs/ui" + +const filterHelper = createDataTableFilterHelper() const filters = [ filterHelper.accessor("status", { @@ -30805,292 +31080,230 @@ export default CustomPage ``` -# Forms - Admin Components - -The Medusa Admin has two types of forms: - -1. Create forms, created using the [FocusModal UI component](https://docs.medusajs.com/ui/components/focus-modal/index.html.md). -2. Edit or update forms, created using the [Drawer UI component](https://docs.medusajs.com/ui/components/drawer/index.html.md). - -This guide explains how to create these two form types following the Medusa Admin's conventions. - -## Form Tooling - -The Medusa Admin uses the following tools to build the forms: - -1. [react-hook-form](https://react-hook-form.com/) to easily build forms and manage their states. -2. [Zod](https://zod.dev/) to validate the form's fields. - -Both of these libraries are available in your project, so you don't have to install them to use them. +# Header - Admin Components -*** +Each section in the Medusa Admin has a header with a title, and optionally a subtitle with buttons to perform an action. -## Create Form +![Example of a header in a section](https://res.cloudinary.com/dza7lstvk/image/upload/v1728288562/Medusa%20Resources/header_dtz4gl.png) -In this section, you'll build a form component to create an item of a resource. +To create a component that uses the same header styling and structure, create the file `src/admin/components/header.tsx` with the following content: -### Full Component +```tsx title="src/admin/components/header.tsx" +import { Heading, Button, Text } from "@medusajs/ui" +import React from "react" +import { Link, LinkProps } from "react-router-dom" +import { ActionMenu, ActionMenuProps } from "./action-menu" -```tsx title="src/admin/components/create-form.tsx" -import { - FocusModal, - Heading, - Label, - Input, - Button, -} from "@medusajs/ui" -import { - useForm, - FormProvider, - Controller, -} from "react-hook-form" -import * as zod from "zod" - -const schema = zod.object({ - name: zod.string(), -}) - -export const CreateForm = () => { - const form = useForm>({ - defaultValues: { - name: "", - }, - }) - - const handleSubmit = form.handleSubmit(({ name }) => { - // TODO submit to backend - console.log(name) - }) +export type HeadingProps = { + title: string + subtitle?: string + actions?: ( + { + type: "button", + props: React.ComponentProps + link?: LinkProps + } | + { + type: "action-menu" + props: ActionMenuProps + } | + { + type: "custom" + children: React.ReactNode + } + )[] +} +export const Header = ({ + title, + subtitle, + actions = [], +}: HeadingProps) => { return ( - - - - - - -
- -
- - - - -
-
- -
-
-
- - Create Item - -
-
- { - return ( -
-
- -
- -
- ) - }} - /> -
-
-
-
-
-
-
-
+
+
+ {title} + {subtitle && ( + + {subtitle} + + )} +
+ {actions.length > 0 && ( +
+ {actions.map((action, index) => ( + <> + {action.type === "button" && ( + + )} + {action.type === "action-menu" && ( + + )} + {action.type === "custom" && action.children} + + ))} +
+ )} +
) } ``` -Unlike other components in this documentation, this form component isn't reusable. You have to create one for every resource that has a create form in the admin. +The `Header` component shows a title, and optionally a subtitle and action buttons. -Start by creating the file `src/admin/components/create-form.tsx` that you'll create the form in. +The component also uses the [Action Menu](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/action-menu/index.html.md) custom component. -### Create Validation Schema +It accepts the following props: -In `src/admin/components/create-form.tsx`, create a validation schema with Zod for the form's fields: +- title: (\`string\`) The section's title. +- subtitle: (\`string\`) The section's subtitle. +- actions: (\`object\[]\`) An array of actions to show. -```tsx title="src/admin/components/create-form.tsx" -import * as zod from "zod" + - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add. -const schema = zod.object({ - name: zod.string(), -}) -``` + \- If its value is \`button\`, it'll show a button that can have a link or an on-click action. -The form in this guide is simple, it only has a required `name` field, which is a string. + \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions. -### Initialize Form + \- If its value is \`custom\`, you can pass any React nodes to render. -Next, you'll initialize the form using `react-hook-form`. + - props: (object) -Add to `src/admin/components/create-form.tsx` the following: + - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions. -```tsx title="src/admin/components/create-form.tsx" -// other imports... -import { useForm } from "react-hook-form" +*** -// validation schema... +## Example -export const CreateForm = () => { - const form = useForm>({ - defaultValues: { - name: "", - }, - }) +Use the `Header` component in any widget or UI route. - const handleSubmit = form.handleSubmit(({ name }) => { - // TODO submit to backend - console.log(name) - }) +For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: - // TODO render form +```tsx title="src/admin/widgets/product-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container } from "../components/container" +import { Header } from "../components/header" + +const ProductWidget = () => { + return ( + +
{ + alert("You clicked the button.") + }, + }, + }, + ]} + /> + + ) } + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget ``` -You create the `CreateForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form. +This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component. -You also define a `handleSubmit` function to perform an action when the form is submitted. -You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) for more details on how to do that. +# Section Row - Admin Components -### Render Components +The Medusa Admin often shows information in rows of label-values, such as when showing a product's details. -You'll now add a `return` statement that renders the focus modal where the form is shown. +![Example of a section row in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728292781/Medusa%20Resources/section-row_kknbnw.png) -Replace `// TODO render form` with the following: +To create a component that shows information in the same structure, create the file `src/admin/components/section-row.tsx` with the following content: -```tsx title="src/admin/components/create-form.tsx" -// other imports... -import { - FocusModal, - Heading, - Label, - Input, - Button, -} from "@medusajs/ui" -import { - FormProvider, - Controller, -} from "react-hook-form" +```tsx title="src/admin/components/section-row.tsx" +import { Text, clx } from "@medusajs/ui" -export const CreateForm = () => { - // ... +export type SectionRowProps = { + title: string + value?: React.ReactNode | string | null + actions?: React.ReactNode +} + +export const SectionRow = ({ title, value, actions }: SectionRowProps) => { + const isValueString = typeof value === "string" || !value return ( - - - - - - -
- -
- - - - -
-
- -
-
-
- - Create Item - -
-
- { - return ( -
-
- -
- -
- ) - }} - /> -
-
-
-
-
-
-
-
+
+ + {title} + + + {isValueString ? ( + + {value ?? "-"} + + ) : ( +
{value}
+ )} + + {actions &&
{actions}
} +
) } ``` -You render a focus modal, with a trigger button to open it. - -In the `FocusModal.Content` component, you wrap the content with the `FormProvider` component from `react-hook-form`, passing it the details of the form you initialized earlier as props. +The `SectionRow` component shows a title and a value in the same row. -In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event. +It accepts the following props: -In the `FocusModal.Header` component, you add buttons to save or cancel the form submission. +- title: (\`string\`) The title to show on the left side. +- value: (\`React.ReactNode\` \\| \`string\` \\| \`null\`) The value to show on the right side. +- actions: (\`React.ReactNode\`) The actions to show at the end of the row. -Finally, you render the form's components inside the `FocusModal.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`. +*** -### Use Create Form Component +## Example -You can use the `CreateForm` component in your widget or UI route. +Use the `SectionRow` component in any widget or UI route. For example, create the widget `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 { CreateForm } from "../components/create-form" import { Container } from "../components/container" import { Header } from "../components/header" +import { SectionRow } from "../components/section-row" const ProductWidget = () => { return ( -
, - }, - ]} - /> +
+ ) } @@ -31102,245 +31315,270 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` -This component uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom components. +This widget also uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. -It will add at the top of a product's details page a new section, and in its header you'll find a Create button. If you click on it, it will open the focus modal with your form. -*** +# JSON View - Admin Components -## Edit Form +Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format. -In this section, you'll build a form component to edit an item of a resource. +![Example of a JSON section in the admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295129/Medusa%20Resources/json_dtbsgm.png) -### Full Component +To create a component that shows a JSON section in your customizations, create the file `src/admin/components/json-view-section.tsx` with the following content: -```tsx title="src/admin/components/edit-form.tsx" -import { +```tsx title="src/admin/components/json-view-section.tsx" +import { + ArrowUpRightOnBox, + Check, + SquareTwoStack, + TriangleDownMini, + XMarkMini, +} from "@medusajs/icons" +import { + Badge, + Container, Drawer, Heading, - Label, - Input, - Button, + IconButton, + Kbd, } from "@medusajs/ui" -import { - useForm, - FormProvider, - Controller, -} from "react-hook-form" -import * as zod from "zod" - -const schema = zod.object({ - name: zod.string(), -}) +import Primitive from "@uiw/react-json-view" +import { CSSProperties, MouseEvent, Suspense, useState } from "react" -export const EditForm = () => { - const form = useForm>({ - defaultValues: { - name: "", - }, - }) +type JsonViewSectionProps = { + data: object + title?: string +} - const handleSubmit = form.handleSubmit(({ name }) => { - // TODO submit to backend - console.log(name) - }) +export const JsonViewSection = ({ data }: JsonViewSectionProps) => { + const numberOfKeys = Object.keys(data).length return ( - - - - - - -
+
+ JSON + + {numberOfKeys} keys + +
+ + + - - - Edit Item - - - - { - return ( -
-
- -
- -
- ) - }} - /> -
- -
+ + + + +
+
+ + + + {numberOfKeys} + + + +
+
+ + esc + - + + + -
- - - - - - ) -} -``` - -Unlike other components in this documentation, this form component isn't reusable. You have to create one for every resource that has an edit form in the admin. - -Start by creating the file `src/admin/components/edit-form.tsx` that you'll create the form in. - -### Create Validation Schema +
+ +
+
} + > + + } /> + ( + null + )} + /> + ( + undefined + )} + /> + { + return ( + + {Object.keys(value as object).length} items + + ) + }} + /> + + + + + : + + { + return + }} + /> + + +
+ +
+
+ + ) +} -In `src/admin/components/edit-form.tsx`, create a validation schema with Zod for the form's fields: +type CopiedProps = { + style?: CSSProperties + value: object | undefined +} -```tsx title="src/admin/components/edit-form.tsx" -import * as zod from "zod" +const Copied = ({ style, value }: CopiedProps) => { + const [copied, setCopied] = useState(false) -const schema = zod.object({ - name: zod.string(), -}) -``` + const handler = (e: MouseEvent) => { + e.stopPropagation() + setCopied(true) -The form in this guide is simple, it only has a required `name` field, which is a string. + if (typeof value === "string") { + navigator.clipboard.writeText(value) + } else { + const json = JSON.stringify(value, null, 2) + navigator.clipboard.writeText(json) + } -### Initialize Form + setTimeout(() => { + setCopied(false) + }, 2000) + } -Next, you'll initialize the form using `react-hook-form`. + const styl = { whiteSpace: "nowrap", width: "20px" } -Add to `src/admin/components/edit-form.tsx` the following: + if (copied) { + return ( + + + + ) + } -```tsx title="src/admin/components/edit-form.tsx" -// other imports... -import { useForm } from "react-hook-form" + return ( + + + + ) +} +``` -// validation schema... +The `JsonViewSection` component shows a section with the "JSON" title and a button to show the data as JSON in a drawer or side window. -export const EditForm = () => { - const form = useForm>({ - defaultValues: { - name: "", - }, - }) +The `JsonViewSection` accepts a `data` prop, which is the data to show as a JSON object in the drawer. - const handleSubmit = form.handleSubmit(({ name }) => { - // TODO submit to backend - console.log(name) - }) +*** - // TODO render form +## Example + +Use the `JsonViewSection` component in any widget or UI route. + +For example, create the widget `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 { JsonViewSection } from "../components/json-view-section" + +const ProductWidget = () => { + return } + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget ``` -You create the `EditForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form. +This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`. -You also define a `handleSubmit` function to perform an action when the form is submitted. -You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) for more details on how to do that. +# Container - Admin Components -### Render Components +The Medusa Admin wraps each section of a page in a container. -You'll now add a `return` statement that renders the drawer where the form is shown. +![Example of a container in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728287102/Medusa%20Resources/container_soenir.png) -Replace `// TODO render form` with the following: +To create a component that uses the same container styling in your widgets or UI routes, create the file `src/admin/components/container.tsx` with the following content: -```tsx title="src/admin/components/edit-form.tsx" -// other imports... +```tsx import { - Drawer, - Heading, - Label, - Input, - Button, + Container as UiContainer, + clx, } from "@medusajs/ui" -import { - FormProvider, - Controller, -} from "react-hook-form" -export const EditForm = () => { - // ... +type ContainerProps = React.ComponentProps +export const Container = (props: ContainerProps) => { return ( - - - - - - -
- - - Edit Item - - - - { - return ( -
-
- -
- -
- ) - }} - /> -
- -
- - - - -
-
-
-
-
-
+ ) } ``` -You render a drawer, with a trigger button to open it. - -In the `Drawer.Content` component, you wrap the content with the `FormProvider` component from `react-hook-form`, passing it the details of the form you initialized earlier as props. - -In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event. - -You render the form's components inside the `Drawer.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`. +The `Container` component re-uses the component from the [Medusa UI package](https://docs.medusajs.com/ui/components/container/index.html.md) and applies to it classes to match the Medusa Admin's design conventions. -Finally, in the `Drawer.Footer` component, you add buttons to save or cancel the form submission. +*** -### Use Edit Form Component +## Example -You can use the `EditForm` component in your widget or UI route. +Use that `Container` component in any widget or UI route. For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: @@ -31348,20 +31586,11 @@ For example, create the widget `src/admin/widgets/product-widget.tsx` with the f import { defineWidgetConfig } from "@medusajs/admin-sdk" import { Container } from "../components/container" import { Header } from "../components/header" -import { EditForm } from "../components/edit-form" const ProductWidget = () => { return ( -
, - }, - ]} - /> +
) } @@ -31373,373 +31602,297 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` -This component uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom components. - -It will add at the top of a product's details page a new section, and in its header you'll find an "Edit Item" button. If you click on it, it will open the drawer with your form. - - -# Header - Admin Components - -Each section in the Medusa Admin has a header with a title, and optionally a subtitle with buttons to perform an action. - -![Example of a header in a section](https://res.cloudinary.com/dza7lstvk/image/upload/v1728288562/Medusa%20Resources/header_dtz4gl.png) - -To create a component that uses the same header styling and structure, create the file `src/admin/components/header.tsx` with the following content: - -```tsx title="src/admin/components/header.tsx" -import { Heading, Button, Text } from "@medusajs/ui" -import React from "react" -import { Link, LinkProps } from "react-router-dom" -import { ActionMenu, ActionMenuProps } from "./action-menu" +This widget also uses a [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. -export type HeadingProps = { - title: string - subtitle?: string - actions?: ( - { - type: "button", - props: React.ComponentProps - link?: LinkProps - } | - { - type: "action-menu" - props: ActionMenuProps - } | - { - type: "custom" - children: React.ReactNode - } - )[] -} -export const Header = ({ - title, - subtitle, - actions = [], -}: HeadingProps) => { - return ( -
-
- {title} - {subtitle && ( - - {subtitle} - - )} -
- {actions.length > 0 && ( -
- {actions.map((action, index) => ( - <> - {action.type === "button" && ( - - )} - {action.type === "action-menu" && ( - - )} - {action.type === "custom" && action.children} - - ))} -
- )} -
- ) -} -``` +# Forms - Admin Components -The `Header` component shows a title, and optionally a subtitle and action buttons. +The Medusa Admin has two types of forms: -The component also uses the [Action Menu](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/action-menu/index.html.md) custom component. +1. Create forms, created using the [FocusModal UI component](https://docs.medusajs.com/ui/components/focus-modal/index.html.md). +2. Edit or update forms, created using the [Drawer UI component](https://docs.medusajs.com/ui/components/drawer/index.html.md). -It accepts the following props: +This guide explains how to create these two form types following the Medusa Admin's conventions. -- title: (\`string\`) The section's title. -- subtitle: (\`string\`) The section's subtitle. -- actions: (\`object\[]\`) An array of actions to show. +## Form Tooling - - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add. +The Medusa Admin uses the following tools to build the forms: - \- If its value is \`button\`, it'll show a button that can have a link or an on-click action. +1. [react-hook-form](https://react-hook-form.com/) to easily build forms and manage their states. +2. [Zod](https://zod.dev/) to validate the form's fields. - \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions. +Both of these libraries are available in your project, so you don't have to install them to use them. - \- If its value is \`custom\`, you can pass any React nodes to render. +*** - - props: (object) +## Create Form - - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions. +In this section, you'll build a form component to create an item of a resource. -*** +### Full Component -## Example +```tsx title="src/admin/components/create-form.tsx" +import { + FocusModal, + Heading, + Label, + Input, + Button, +} from "@medusajs/ui" +import { + useForm, + FormProvider, + Controller, +} from "react-hook-form" +import * as zod from "zod" -Use the `Header` component in any widget or UI route. +const schema = zod.object({ + name: zod.string(), +}) -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: +export const CreateForm = () => { + const form = useForm>({ + defaultValues: { + name: "", + }, + }) -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container } from "../components/container" -import { Header } from "../components/header" + const handleSubmit = form.handleSubmit(({ name }) => { + // TODO submit to backend + console.log(name) + }) -const ProductWidget = () => { return ( - -
{ - alert("You clicked the button.") - }, - }, - }, - ]} - /> - + + + + + + +
+ +
+ + + + +
+
+ +
+
+
+ + Create Item + +
+
+ { + return ( +
+
+ +
+ +
+ ) + }} + /> +
+
+
+
+
+
+
+
) } - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget ``` -This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component. +Unlike other components in this documentation, this form component isn't reusable. You have to create one for every resource that has a create form in the admin. +Start by creating the file `src/admin/components/create-form.tsx` that you'll create the form in. -# JSON View - Admin Components +### Create Validation Schema -Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format. +In `src/admin/components/create-form.tsx`, create a validation schema with Zod for the form's fields: -![Example of a JSON section in the admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295129/Medusa%20Resources/json_dtbsgm.png) +```tsx title="src/admin/components/create-form.tsx" +import * as zod from "zod" -To create a component that shows a JSON section in your customizations, create the file `src/admin/components/json-view-section.tsx` with the following content: +const schema = zod.object({ + name: zod.string(), +}) +``` -```tsx title="src/admin/components/json-view-section.tsx" -import { - ArrowUpRightOnBox, - Check, - SquareTwoStack, - TriangleDownMini, - XMarkMini, -} from "@medusajs/icons" -import { - Badge, - Container, - Drawer, - Heading, - IconButton, - Kbd, -} from "@medusajs/ui" -import Primitive from "@uiw/react-json-view" -import { CSSProperties, MouseEvent, Suspense, useState } from "react" +The form in this guide is simple, it only has a required `name` field, which is a string. -type JsonViewSectionProps = { - data: object - title?: string -} +### Initialize Form -export const JsonViewSection = ({ data }: JsonViewSectionProps) => { - const numberOfKeys = Object.keys(data).length +Next, you'll initialize the form using `react-hook-form`. - return ( - -
- JSON - - {numberOfKeys} keys - -
- - - - - - - -
-
- - - - {numberOfKeys} - - - -
-
- - esc - - - - - - -
-
- -
-
} - > - - } /> - ( - null - )} - /> - ( - undefined - )} - /> - { - return ( - - {Object.keys(value as object).length} items - - ) - }} - /> - - - - - : - - { - return - }} - /> - - - -
-
-
-
- ) -} +Add to `src/admin/components/create-form.tsx` the following: + +```tsx title="src/admin/components/create-form.tsx" +// other imports... +import { useForm } from "react-hook-form" -type CopiedProps = { - style?: CSSProperties - value: object | undefined +// validation schema... + +export const CreateForm = () => { + const form = useForm>({ + defaultValues: { + name: "", + }, + }) + + const handleSubmit = form.handleSubmit(({ name }) => { + // TODO submit to backend + console.log(name) + }) + + // TODO render form } +``` -const Copied = ({ style, value }: CopiedProps) => { - const [copied, setCopied] = useState(false) +You create the `CreateForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form. - const handler = (e: MouseEvent) => { - e.stopPropagation() - setCopied(true) +You also define a `handleSubmit` function to perform an action when the form is submitted. - if (typeof value === "string") { - navigator.clipboard.writeText(value) - } else { - const json = JSON.stringify(value, null, 2) - navigator.clipboard.writeText(json) - } +You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) for more details on how to do that. - setTimeout(() => { - setCopied(false) - }, 2000) - } +### Render Components - const styl = { whiteSpace: "nowrap", width: "20px" } +You'll now add a `return` statement that renders the focus modal where the form is shown. - if (copied) { - return ( - - - - ) - } +Replace `// TODO render form` with the following: + +```tsx title="src/admin/components/create-form.tsx" +// other imports... +import { + FocusModal, + Heading, + Label, + Input, + Button, +} from "@medusajs/ui" +import { + FormProvider, + Controller, +} from "react-hook-form" + +export const CreateForm = () => { + // ... return ( - - - + + + + + + +
+ +
+ + + + +
+
+ +
+
+
+ + Create Item + +
+
+ { + return ( +
+
+ +
+ +
+ ) + }} + /> +
+
+
+
+
+
+
+
) } ``` -The `JsonViewSection` component shows a section with the "JSON" title and a button to show the data as JSON in a drawer or side window. +You render a focus modal, with a trigger button to open it. -The `JsonViewSection` accepts a `data` prop, which is the data to show as a JSON object in the drawer. +In the `FocusModal.Content` component, you wrap the content with the `FormProvider` component from `react-hook-form`, passing it the details of the form you initialized earlier as props. -*** +In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event. -## Example +In the `FocusModal.Header` component, you add buttons to save or cancel the form submission. -Use the `JsonViewSection` component in any widget or UI route. +Finally, you render the form's components inside the `FocusModal.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`. + +### Use Create Form Component + +You can use the `CreateForm` component in your widget or UI route. For example, create the widget `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 { JsonViewSection } from "../components/json-view-section" +import { CreateForm } from "../components/create-form" +import { Container } from "../components/container" +import { Header } from "../components/header" const ProductWidget = () => { - return + return ( + +
, + }, + ]} + /> + + ) } export const config = defineWidgetConfig({ @@ -31749,311 +31902,263 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` -This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`. - - -# Section Row - Admin Components - -The Medusa Admin often shows information in rows of label-values, such as when showing a product's details. - -![Example of a section row in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728292781/Medusa%20Resources/section-row_kknbnw.png) - -To create a component that shows information in the same structure, create the file `src/admin/components/section-row.tsx` with the following content: - -```tsx title="src/admin/components/section-row.tsx" -import { Text, clx } from "@medusajs/ui" +This component uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom components. + +It will add at the top of a product's details page a new section, and in its header you'll find a Create button. If you click on it, it will open the focus modal with your form. + +*** + +## Edit Form + +In this section, you'll build a form component to edit an item of a resource. + +### Full Component + +```tsx title="src/admin/components/edit-form.tsx" +import { + Drawer, + Heading, + Label, + Input, + Button, +} from "@medusajs/ui" +import { + useForm, + FormProvider, + Controller, +} from "react-hook-form" +import * as zod from "zod" + +const schema = zod.object({ + name: zod.string(), +}) + +export const EditForm = () => { + const form = useForm>({ + defaultValues: { + name: "", + }, + }) + + const handleSubmit = form.handleSubmit(({ name }) => { + // TODO submit to backend + console.log(name) + }) + + return ( + + + + + + +
+ + + Edit Item + + + + { + return ( +
+
+ +
+ +
+ ) + }} + /> +
+ +
+ + + + +
+
+
+
+
+
+ ) +} +``` + +Unlike other components in this documentation, this form component isn't reusable. You have to create one for every resource that has an edit form in the admin. -export type SectionRowProps = { - title: string - value?: React.ReactNode | string | null - actions?: React.ReactNode -} +Start by creating the file `src/admin/components/edit-form.tsx` that you'll create the form in. -export const SectionRow = ({ title, value, actions }: SectionRowProps) => { - const isValueString = typeof value === "string" || !value +### Create Validation Schema - return ( -
- - {title} - +In `src/admin/components/edit-form.tsx`, create a validation schema with Zod for the form's fields: - {isValueString ? ( - - {value ?? "-"} - - ) : ( -
{value}
- )} +```tsx title="src/admin/components/edit-form.tsx" +import * as zod from "zod" - {actions &&
{actions}
} -
- ) -} +const schema = zod.object({ + name: zod.string(), +}) ``` -The `SectionRow` component shows a title and a value in the same row. +The form in this guide is simple, it only has a required `name` field, which is a string. -It accepts the following props: +### Initialize Form -- title: (\`string\`) The title to show on the left side. -- value: (\`React.ReactNode\` \\| \`string\` \\| \`null\`) The value to show on the right side. -- actions: (\`React.ReactNode\`) The actions to show at the end of the row. +Next, you'll initialize the form using `react-hook-form`. -*** +Add to `src/admin/components/edit-form.tsx` the following: -## Example +```tsx title="src/admin/components/edit-form.tsx" +// other imports... +import { useForm } from "react-hook-form" -Use the `SectionRow` component in any widget or UI route. +// validation schema... -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: +export const EditForm = () => { + const form = useForm>({ + defaultValues: { + name: "", + }, + }) -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container } from "../components/container" -import { Header } from "../components/header" -import { SectionRow } from "../components/section-row" + const handleSubmit = form.handleSubmit(({ name }) => { + // TODO submit to backend + console.log(name) + }) -const ProductWidget = () => { - return ( - -
- - - ) + // TODO render form } - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget ``` -This widget also uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. +You create the `EditForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form. +You also define a `handleSubmit` function to perform an action when the form is submitted. -# Action Menu - Admin Components +You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) for more details on how to do that. -The Medusa Admin often provides additional actions in a dropdown shown when users click a three-dot icon. +### Render Components -![Example of an action menu in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728291319/Medusa%20Resources/action-menu_jnus6k.png) +You'll now add a `return` statement that renders the drawer where the form is shown. -To create a component that shows this menu in your customizations, create the file `src/admin/components/action-menu.tsx` with the following content: +Replace `// TODO render form` with the following: -```tsx title="src/admin/components/action-menu.tsx" +```tsx title="src/admin/components/edit-form.tsx" +// other imports... import { - DropdownMenu, - IconButton, - clx, + Drawer, + Heading, + Label, + Input, + Button, } from "@medusajs/ui" -import { EllipsisHorizontal } from "@medusajs/icons" -import { Link } from "react-router-dom" - -export type Action = { - icon: React.ReactNode - label: string - disabled?: boolean -} & ( - | { - to: string - onClick?: never - } - | { - onClick: () => void - to?: never - } -) - -export type ActionGroup = { - actions: Action[] -} +import { + FormProvider, + Controller, +} from "react-hook-form" -export type ActionMenuProps = { - groups: ActionGroup[] -} +export const EditForm = () => { + // ... -export const ActionMenu = ({ groups }: ActionMenuProps) => { return ( - - - - - - - - {groups.map((group, index) => { - if (!group.actions.length) { - return null - } - - const isLast = index === groups.length - 1 - - return ( - - {group.actions.map((action, index) => { - if (action.onClick) { - return ( - { - e.stopPropagation() - action.onClick() - }} - className={clx( - "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2", - { - "[&_svg]:text-ui-fg-disabled": action.disabled, - } - )} - > - {action.icon} - {action.label} - - ) - } - + + + + + + +
+ + + Edit Item + + + + { return ( -
- - e.stopPropagation()}> - {action.icon} - {action.label} - - +
+
+ +
+
) - })} - {!isLast && } - - ) - })} - - + }} + /> + + +
+ + + + +
+
+ + + + ) } ``` -The `ActionMenu` component shows a three-dots icon (or `EllipsisHorizontal`) from the [Medusa Icons package](https://docs.medusajs.com/ui/icons/overview/index.html.md) in a button. - -When the button is clicked, a dropdown menu is shown with the actions passed in the props. - -The component accepts the following props: - -- groups: (\`object\[]\`) Groups of actions to be shown in the dropdown. Each group is separated by a divider. - - - actions: (\`object\[]\`) Actions in the group. - - - icon: (\`React.ReactNode\`) - - - label: (\`string\`) The action's text. +You render a drawer, with a trigger button to open it. - - disabled: (\`boolean\`) Whether the action is shown as disabled. +In the `Drawer.Content` component, you wrap the content with the `FormProvider` component from `react-hook-form`, passing it the details of the form you initialized earlier as props. - - \`to\`: (\`string\`) The link to take the user to when they click the action. This is required if \`onClick\` isn't provided. +In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event. - - \`onClick\`: (\`() => void\`) The function to execute when the action is clicked. This is required if \`to\` isn't provided. +You render the form's components inside the `Drawer.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`. -*** +Finally, in the `Drawer.Footer` component, you add buttons to save or cancel the form submission. -## Example +### Use Edit Form Component -Use the `ActionMenu` component in any widget or UI route. +You can use the `EditForm` component in your widget or UI route. For example, create the widget `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 { Pencil } from "@medusajs/icons" -import { Container } from "../components/container" -import { ActionMenu } from "../components/action-menu" - -const ProductWidget = () => { - return ( - - , - label: "Edit", - onClick: () => { - alert("You clicked the edit action!") - }, - }, - ], - }, - ]} /> - - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component. - -### Use in Header - -You can also use the action menu in the [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) component as part of its actions. - -For example: - -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Pencil } from "@medusajs/icons" import { Container } from "../components/container" import { Header } from "../components/header" +import { EditForm } from "../components/edit-form" const ProductWidget = () => { return ( -
, - label: "Edit", - onClick: () => { - alert("You clicked the edit action!") - }, - }, - ], - }, - ], - }, + type: "custom", + children: , }, ]} /> @@ -32068,84 +32173,9 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` +This component uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom components. -# Two Column Layout - Admin Components - -The Medusa Admin has pages with two columns of content. - -This doesn't include the sidebar, only the main content. - -![An example of an admin page with two columns](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286690/Medusa%20Resources/two-column_sdnkg0.png) - -To create a layout that you can use in UI routes to support two columns of content, create the component `src/admin/layouts/two-column.tsx` with the following content: - -```tsx title="src/admin/layouts/two-column.tsx" -export type TwoColumnLayoutProps = { - firstCol: React.ReactNode - secondCol: React.ReactNode -} - -export const TwoColumnLayout = ({ - firstCol, - secondCol, -}: TwoColumnLayoutProps) => { - return ( -
-
- {firstCol} -
-
- {secondCol} -
-
- ) -} -``` - -The `TwoColumnLayout` accepts two props: - -- `firstCol` indicating the content of the first column. -- `secondCol` indicating the content of the second column. - -*** - -## Example - -Use the `TwoColumnLayout` component in your UI routes that have a single column. For example: - -```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]} -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" -import { Container } from "../../components/container" -import { Header } from "../../components/header" -import { TwoColumnLayout } from "../../layouts/two-column" - -const CustomPage = () => { - return ( - -
- - } - secondCol={ - -
- - } - /> - ) -} - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - -This UI route also uses [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header]() custom components. +It will add at the top of a product's details page a new section, and in its header you'll find an "Edit Item" button. If you click on it, it will open the drawer with your form. # Table - Admin Components @@ -32580,70 +32610,168 @@ const posts = await postModuleService.listPosts({ }) ``` -The `dateTime` property also stores the time. So, when matching for an exact day, you must set a range filter to be between the beginning and end of the day. +The `dateTime` property also stores the time. So, when matching for an exact day, you must set a range filter to be between the beginning and end of the day. + +In this example, you retrieve the current date twice: once to set its time to `00:00:00`, and another to set its time `23:59:59`. Then, you retrieve posts whose `published_at` property is between `00:00:00` and `23:59:59` of today. + +*** + +## Apply Or Condition + +```ts +const posts = await postModuleService.listPosts({ + $or: [ + { + name: "My Post", + }, + { + published_at: { + $lt: new Date(), + }, + }, + ], +}) +``` + +To use an `or` condition, pass to the filter object the `$or` property, whose value is an array of filters. + +In the example above, posts whose name is `My Post` or their `published_at` date is less than the current date and time are retrieved. + + +# listAndCount Method - Service Factory Reference + +This method retrieves a list of records with the total count. + +## Retrieve List of Records + +```ts +const [posts, count] = await postModuleService.listAndCountPosts() +``` + +If no parameters are passed, the method returns an array with two items: + +1. The first is an array of the first `15` records retrieved. +2. The second is the total count of records. + +*** + +## Filter Records + +```ts +const [posts, count] = await postModuleService.listAndCountPosts({ + id: ["123", "321"], +}) +``` + +### Parameters + +To retrieve records matching a set of filters, pass an object of the filters as a first parameter. + +Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). + +### Returns + +The method returns an array with two items: + +1. The first is an array of the first `15` records retrieved matching the specified filters. +2. The second is the total count of records matching the specified filters. + +*** + +## Retrieve Relations + +This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). + +```ts +const [posts, count] = await postModuleService.listAndCountPosts({}, { + relations: ["author"], +}) +``` + +### Parameters -In this example, you retrieve the current date twice: once to set its time to `00:00:00`, and another to set its time `23:59:59`. Then, you retrieve posts whose `published_at` property is between `00:00:00` and `23:59:59` of today. +To retrieve records with their relations, pass as a second parameter an object having a `relations` property. Its value is an array of relation names. + +### Returns + +The method returns an array with two items: + +1. The first is an array of the first `15` records retrieved. +2. The second is the total count of records. *** -## Apply Or Condition +## Select Properties ```ts -const posts = await postModuleService.listPosts({ - $or: [ - { - name: "My Post", - }, - { - published_at: { - $lt: new Date(), - }, - }, - ], +const [posts, count] = await postModuleService.listAndCountPosts({}, { + select: ["id", "name"], }) ``` -To use an `or` condition, pass to the filter object the `$or` property, whose value is an array of filters. +### Parameters -In the example above, posts whose name is `My Post` or their `published_at` date is less than the current date and time are retrieved. +By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property. +`select`'s value is an array of property names to retrieve. -# create Method - Service Factory Reference +### Returns -This method creates one or more records of the data model. +The method returns an array with two items: -## Create One Record +1. The first is an array of the first `15` records retrieved. +2. The second is the total count of records. + +*** + +## Paginate Relations ```ts -const post = await postModuleService.createPosts({ - name: "My Post", - published_at: new Date(), - metadata: { - external_id: "1234", - }, +const [posts, count] = await postModuleService.listAndCountPosts({}, { + take: 20, + skip: 10, }) ``` -If an object is passed of the method, an object of the created record is also returned. +### Parameters + +To paginate the returned records, the second object parameter accepts the following properties: + +- `take`: a number indicating how many records to retrieve. By default, it's `15`. +- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`. + +### Returns + +The method returns an array with two items: + +1. The first is an array of the records retrieved. The number of records is less than or equal to `take`'s value. +2. The second is the total count of records. *** -## Create Multiple Records +## Sort Records ```ts -const posts = await postModuleService.createPosts([ - { - name: "My Post", - published_at: new Date(), - }, - { - name: "My Other Post", - published_at: new Date(), +const [posts, count] = await postModuleService.listAndCountPosts({}, { + order: { + name: "ASC", }, -]) +}) ``` -If an array is passed of the method, an array of the created records is also returned. +### Parameters + +To sort records by one or more properties, pass to the second object parameter the `order` property. Its value is an object whose keys are the property names, and values can either be: + +- `ASC` to sort by this property in the ascending order. +- `DESC` to sort by this property in the descending order. + +### Returns + +The method returns an array with two items: + +1. The first is an array of the first `15` records retrieved. +2. The second is the total count of records. # delete Method - Service Factory Reference @@ -32686,27 +32814,24 @@ To delete records matching a set of filters, pass an object of filters as a para Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). -# listAndCount Method - Service Factory Reference +# list Method - Service Factory Reference -This method retrieves a list of records with the total count. +This method retrieves a list of records. ## Retrieve List of Records ```ts -const [posts, count] = await postModuleService.listAndCountPosts() +const posts = await postModuleService.listPosts() ``` -If no parameters are passed, the method returns an array with two items: - -1. The first is an array of the first `15` records retrieved. -2. The second is the total count of records. +If no parameters are passed, the method returns an array of the first `15` records. *** ## Filter Records ```ts -const [posts, count] = await postModuleService.listAndCountPosts({ +const posts = await postModuleService.listPosts({ id: ["123", "321"], }) ``` @@ -32719,10 +32844,7 @@ Learn more about accepted filters in [this documentation](https://docs.medusajs. ### Returns -The method returns an array with two items: - -1. The first is an array of the first `15` records retrieved matching the specified filters. -2. The second is the total count of records matching the specified filters. +The method returns an array of the first `15` records matching the filters. *** @@ -32731,28 +32853,25 @@ The method returns an array with two items: This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). ```ts -const [posts, count] = await postModuleService.listAndCountPosts({}, { +const posts = await postModuleService.listPosts({}, { relations: ["author"], }) ``` ### Parameters -To retrieve records with their relations, pass as a second parameter an object having a `relations` property. Its value is an array of relation names. +To retrieve records with their relations, pass as a second parameter an object having a `relations` property. `relations`'s value is an array of relation names. ### Returns -The method returns an array with two items: - -1. The first is an array of the first `15` records retrieved. -2. The second is the total count of records. +The method returns an array of the first `15` records matching the filters. *** ## Select Properties ```ts -const [posts, count] = await postModuleService.listAndCountPosts({}, { +const posts = await postModuleService.listPosts({}, { select: ["id", "name"], }) ``` @@ -32765,17 +32884,14 @@ By default, retrieved records have all their properties. To select specific prop ### Returns -The method returns an array with two items: - -1. The first is an array of the first `15` records retrieved. -2. The second is the total count of records. +The method returns an array of the first `15` records matching the filters. *** ## Paginate Relations ```ts -const [posts, count] = await postModuleService.listAndCountPosts({}, { +const posts = await postModuleService.listPosts({}, { take: 20, skip: 10, }) @@ -32790,17 +32906,14 @@ To paginate the returned records, the second object parameter accepts the follow ### Returns -The method returns an array with two items: - -1. The first is an array of the records retrieved. The number of records is less than or equal to `take`'s value. -2. The second is the total count of records. +The method returns an array of records. The number of records is less than or equal to `take`'s value. *** ## Sort Records ```ts -const [posts, count] = await postModuleService.listAndCountPosts({}, { +const posts = await postModuleService.listPosts({}, { order: { name: "ASC", }, @@ -32816,128 +32929,219 @@ To sort records by one or more properties, pass to the second object parameter t ### Returns -The method returns an array with two items: - -1. The first is an array of the first `15` records retrieved. -2. The second is the total count of records. +The method returns an array of the first `15` records matching the filters. -# list Method - Service Factory Reference +# create Method - Service Factory Reference -This method retrieves a list of records. +This method creates one or more records of the data model. -## Retrieve List of Records +## Create One Record ```ts -const posts = await postModuleService.listPosts() +const post = await postModuleService.createPosts({ + name: "My Post", + published_at: new Date(), + metadata: { + external_id: "1234", + }, +}) ``` -If no parameters are passed, the method returns an array of the first `15` records. +If an object is passed of the method, an object of the created record is also returned. *** -## Filter Records +## Create Multiple Records ```ts -const posts = await postModuleService.listPosts({ - id: ["123", "321"], -}) +const posts = await postModuleService.createPosts([ + { + name: "My Post", + published_at: new Date(), + }, + { + name: "My Other Post", + published_at: new Date(), + }, +]) ``` -### Parameters +If an array is passed of the method, an array of the created records is also returned. -To retrieve records matching a set of filters, pass an object of the filters as a first parameter. -Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). +# restore Method - Service Factory Reference + +This method restores one or more records of the data model that were [soft-deleted](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/soft-delete/index.html.md). + +## Restore One Record + +```ts +const restoredPosts = await postModuleService.restorePosts("123") +``` + +### Parameters + +To restore one record, pass its ID as a parameter of the method. ### Returns -The method returns an array of the first `15` records matching the filters. +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. + +For example, the returned object of the above example is: + +```ts +restoredPosts = { + post_id: ["123"], +} +``` *** -## Retrieve Relations +## Restore Multiple Records -This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). +```ts +const restoredPosts = await postModuleService.restorePosts([ + "123", + "321", +]) +``` + +### Parameters + +To restore multiple records, pass an array of IDs as a parameter of the method. + +### Returns + +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. + +For example, the returned object of the above example is: ```ts -const posts = await postModuleService.listPosts({}, { - relations: ["author"], +restoredPosts = { + post_id: [ + "123", + "321", + ], +} +``` + +*** + +## Restore Records Matching Filters + +```ts +const restoredPosts = await postModuleService.restorePosts({ + name: "My Post", }) ``` -### Parameters - -To retrieve records with their relations, pass as a second parameter an object having a `relations` property. `relations`'s value is an array of relation names. - -### Returns +### Parameters + +To restore records matching a set of filters, pass an object of fitlers as a parameter of the method. + +Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). + +### Returns + +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. + +For example, the returned object of the above example is: + +```ts +restoredPosts = { + post_id: [ + "123", + ], +} +``` + -The method returns an array of the first `15` records matching the filters. +# softDelete Method - Service Factory Reference -*** +This method soft deletes one or more records of the data model. -## Select Properties +## Soft Delete One Record ```ts -const posts = await postModuleService.listPosts({}, { - select: ["id", "name"], -}) +const deletedPosts = await postModuleService.softDeletePosts( + "123" +) ``` ### Parameters -By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property. - -`select`'s value is an array of property names to retrieve. +To soft delete a record, pass its ID as a parameter of the method. ### Returns -The method returns an array of the first `15` records matching the filters. +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs. + +For example, the returned object of the above example is: + +```ts +deletedPosts = { + post_id: ["123"], +} +``` *** -## Paginate Relations +## Soft Delete Multiple Records ```ts -const posts = await postModuleService.listPosts({}, { - take: 20, - skip: 10, -}) +const deletedPosts = await postModuleService.softDeletePosts([ + "123", + "321", +]) ``` ### Parameters -To paginate the returned records, the second object parameter accepts the following properties: - -- `take`: a number indicating how many records to retrieve. By default, it's `15`. -- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`. +To soft delete multiple records, pass an array of IDs as a parameter of the method. ### Returns -The method returns an array of records. The number of records is less than or equal to `take`'s value. +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs. + +For example, the returned object of the above example is: + +```ts +deletedPosts = { + post_id: [ + "123", + "321", + ], +} +``` *** -## Sort Records +## Soft Delete Records Matching Filters ```ts -const posts = await postModuleService.listPosts({}, { - order: { - name: "ASC", - }, +const deletedPosts = await postModuleService.softDeletePosts({ + name: "My Post", }) ``` ### Parameters -To sort records by one or more properties, pass to the second object parameter the `order` property. Its value is an object whose keys are the property names, and values can either be: +To soft delete records matching a set of filters, pass an object of filters as a parameter. -- `ASC` to sort by this property in the ascending order. -- `DESC` to sort by this property in the descending order. +Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). ### Returns -The method returns an array of the first `15` records matching the filters. +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs. + +For example, the returned object of the above example is: + +```ts +deletedPosts = { + post_id: ["123"], +} +``` # retrieve Method - Service Factory Reference @@ -33120,180 +33324,6 @@ Learn more about accepted filters in [this documentation](https://docs.medusajs. The method returns an array of objects of updated records. -# softDelete Method - Service Factory Reference - -This method soft deletes one or more records of the data model. - -## Soft Delete One Record - -```ts -const deletedPosts = await postModuleService.softDeletePosts( - "123" -) -``` - -### Parameters - -To soft delete a record, pass its ID as a parameter of the method. - -### Returns - -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs. - -For example, the returned object of the above example is: - -```ts -deletedPosts = { - post_id: ["123"], -} -``` - -*** - -## Soft Delete Multiple Records - -```ts -const deletedPosts = await postModuleService.softDeletePosts([ - "123", - "321", -]) -``` - -### Parameters - -To soft delete multiple records, pass an array of IDs as a parameter of the method. - -### Returns - -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs. - -For example, the returned object of the above example is: - -```ts -deletedPosts = { - post_id: [ - "123", - "321", - ], -} -``` - -*** - -## Soft Delete Records Matching Filters - -```ts -const deletedPosts = await postModuleService.softDeletePosts({ - name: "My Post", -}) -``` - -### Parameters - -To soft delete records matching a set of filters, pass an object of filters as a parameter. - -Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). - -### Returns - -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs. - -For example, the returned object of the above example is: - -```ts -deletedPosts = { - post_id: ["123"], -} -``` - - -# restore Method - Service Factory Reference - -This method restores one or more records of the data model that were [soft-deleted](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/soft-delete/index.html.md). - -## Restore One Record - -```ts -const restoredPosts = await postModuleService.restorePosts("123") -``` - -### Parameters - -To restore one record, pass its ID as a parameter of the method. - -### Returns - -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. - -For example, the returned object of the above example is: - -```ts -restoredPosts = { - post_id: ["123"], -} -``` - -*** - -## Restore Multiple Records - -```ts -const restoredPosts = await postModuleService.restorePosts([ - "123", - "321", -]) -``` - -### Parameters - -To restore multiple records, pass an array of IDs as a parameter of the method. - -### Returns - -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. - -For example, the returned object of the above example is: - -```ts -restoredPosts = { - post_id: [ - "123", - "321", - ], -} -``` - -*** - -## Restore Records Matching Filters - -```ts -const restoredPosts = await postModuleService.restorePosts({ - name: "My Post", -}) -``` - -### Parameters - -To restore records matching a set of filters, pass an object of fitlers as a parameter of the method. - -Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). - -### Returns - -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. - -For example, the returned object of the above example is: - -```ts -restoredPosts = { - post_id: [ - "123", - ], -} -``` - -

Just Getting Started?

diff --git a/www/apps/resources/app/commerce-modules/api-key/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/api-key/links-to-other-modules/page.mdx index 083bf64a35141..065a0ee71225b 100644 --- a/www/apps/resources/app/commerce-modules/api-key/links-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/api-key/links-to-other-modules/page.mdx @@ -1,4 +1,4 @@ -import { CodeTabs, CodeTab } from "docs-ui" +import { CodeTabs, CodeTab, Table } from "docs-ui" export const metadata = { title: `Links between API Key Module and Other Modules`, @@ -12,7 +12,40 @@ This document showcases the module links defined between the API Key Module and The API Key Module has the following links to other modules: -- [`ApiKey` data model \<\> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). + + + + + First Data Model + + + Second Data Model + + + Type + + + Description + + + + + + + [ApiKey](/references/api-key/models/ApiKey) + + + [SalesChannel](/references/sales-channel/models/SalesChannel) in [Sales Channel Module](../../sales-channel/page.mdx) + + + Stored + + + [Learn more](#sales-channel-module) + + + +
--- diff --git a/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/page.mdx index 083b94f6a1ba1..150ced9899b24 100644 --- a/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/page.mdx @@ -1,4 +1,4 @@ -import { CodeTabs, CodeTab } from "docs-ui" +import { CodeTabs, CodeTab, Table } from "docs-ui" export const metadata = { title: `Links between Cart Module and Other Modules`, @@ -18,14 +18,140 @@ Read-only links are used to query data across modules, but the relations aren't -- [`Cart` data model \<\> `Customer` data model of Customer Module](#customer-module). (Read-only). -- [`Order` data model of Order Module \<\> `Cart` data model](#order-module). -- [`Cart` data model \<\> `PaymentCollection` data model of Payment Module](#payment-module). -- [`LineItem` data model \<\> `Product` data model of Product Module](#product-module). (Read-only). -- [`LineItem` data model \<\> `ProductVariant` data model of Product Module](#product-module). (Read-only). -- [`Cart` data model \<\> `Promotion` data model of Promotion Module](#promotion-module). -- [`Cart` data model \<\> `Region` data model of Region Module](#region-module). (Read-only). -- [`Cart` data model \<\> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). (Read-only). + + + + + First Data Model + + + Second Data Model + + + Type + + + Description + + + + + + + [Cart](/references/cart/models/Cart) + + + [Customer](/references/customer/models/Customer) in [Customer Module](../../customer/page.mdx) + + + Read-only + + + [Learn more](#customer-module) + + + + + [Order](/references/order/models/Order) in [Order Module](../../order/page.mdx) + + + [Cart](/references/cart/models/Cart) + + + Stored + + + [Learn more](#order-module) + + + + + [Cart](/references/cart/models/Cart) + + + [PaymentCollection](/references/payment/models/PaymentCollection) in [Payment Module](../../payment/page.mdx) + + + Stored + + + [Learn more](#payment-module) + + + + + [LineItem](/references/cart/models/LineItem) + + + [Product](/references/product/models/Product) in [Product Module](../../product/page.mdx) + + + Read-only + + + [Learn more](#product-module) + + + + + [LineItem](/references/cart/models/LineItem) + + + [ProductVariant](/references/product/models/ProductVariant) in [Product Module](../../product/page.mdx) + + + Read-only + + + [Learn more](#product-module) + + + + + [Cart](/references/cart/models/Cart) + + + [Promotion](/references/promotion/models/Promotion) in [Promotion Module](../../promotion/page.mdx) + + + Stored + + + [Learn more](#promotion-module) + + + + + [Cart](/references/cart/models/Cart) + + + [Region](/references/region/models/Region) in [Region Module](../../region/page.mdx) + + + Read-only + + + [Learn more](#region-module) + + + + + [Cart](/references/cart/models/Cart) + + + [SalesChannel](/references/sales-channel/models/SalesChannel) in [Sales Channel Module](../../sales-channel/page.mdx) + + + Read-only + + + [Learn more](#sales-channel-module) + + + +
+ + --- diff --git a/www/apps/resources/app/commerce-modules/currency/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/currency/links-to-other-modules/page.mdx index 0cc6da0aecb28..5776669c5c783 100644 --- a/www/apps/resources/app/commerce-modules/currency/links-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/currency/links-to-other-modules/page.mdx @@ -1,4 +1,4 @@ -import { CodeTabs, CodeTab } from "docs-ui" +import { CodeTabs, CodeTab, Table } from "docs-ui" export const metadata = { title: `Links between Currency Module and Other Modules`, @@ -18,15 +18,47 @@ Read-only links are used to query data across modules, but the relations aren't -- [`Currency` data model of Store Module \<\> `Currency` data model of Currency Module](#store-module). (Read-only). - + + + + + First Data Model + + + Second Data Model + + + Type + + + Description + + + + + + + [Store](/references/store/models/Store) in [Store Module](../../store/page.mdx) + + + [Currency](/references/currency/models/Currency) + + + Read-only + + + [Learn more](#store-module) + + + +
--- ## Store Module The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. -Instead, Medusa defines a read-only link between the Currency Module's `Currency` data model and the [Store Module](../../store/page.mdx)'s `Currency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database. The currencies of a store are determined by the `currency_code` of the `Currency` data model in the Store Module. +Instead, Medusa defines a read-only link between the [Store Module](../../store/page.mdx)'s `Currency` data model and the Currency Module's `Currency` data model. Because the link is read-only from the `Store`'s side, you can only retrieve the details of a store's supported currencies, and not the other way around. ### Retrieve with Query diff --git a/www/apps/resources/app/commerce-modules/customer/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/customer/links-to-other-modules/page.mdx index 67657e8069078..a3870331f48b8 100644 --- a/www/apps/resources/app/commerce-modules/customer/links-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/customer/links-to-other-modules/page.mdx @@ -1,4 +1,4 @@ -import { CodeTabs, CodeTab } from "docs-ui" +import { CodeTabs, CodeTab, Table } from "docs-ui" export const metadata = { title: `Links between Customer Module and Other Modules`, @@ -18,9 +18,69 @@ Read-only links are used to query data across modules, but the relations aren't -- [`Customer` data model \<\> `AccountHolder` data model of Payment Module](#payment-module). -- [`Cart` data model of Cart Module \<\> `Customer` data model](#cart-module). (Read-only). -- [`Order` data model of Order Module \<\> `Customer` data model](#order-module). (Read-only). + + + + + First Data Model + + + Second Data Model + + + Type + + + Description + + + + + + + [Customer](/references/customer/models/Customer) + + + [AccountHolder](/references/payment/models/AccountHolder) in [Payment Module](../../payment/page.mdx) + + + Stored + + + [Learn more](#payment-module) + + + + + [Cart](/references/cart/models/Cart) in [Cart Module](../../cart/page.mdx) + + + [Customer](/references/customer/models/Customer) + + + Read-only + + + [Learn more](#cart-module) + + + + + [Order](/references/order/models/Order) in [Order Module](../../order/page.mdx) + + + [Customer](/references/customer/models/Customer) + + + Read-only + + + [Learn more](#order-module) + + + +
+ --- @@ -120,24 +180,24 @@ createRemoteLinkStep({ ## Cart Module -Medusa defines a read-only link between the `Customer` data model and the [Cart Module](../../cart/page.mdx)'s `Cart` data model. This means you can retrieve the details of a customer's carts, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model. +Medusa defines a read-only link between the [Cart Module](../../cart/page.mdx)'s `Cart` data model and the `Customer` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the customer of a cart, and not the other way around. ### Retrieve with Query -To retrieve a customer's carts with [Query](!docs!/learn/fundamentals/module-links/query), pass `carts.*` in `fields`: +To retrieve the customer of a cart with [Query](!docs!/learn/fundamentals/module-links/query), pass `customer.*` in `fields`: ```ts -const { data: customers } = await query.graph({ - entity: "customer", +const { data: carts } = await query.graph({ + entity: "cart", fields: [ - "carts.*", + "customer.*", ], }) -// customers.carts +// carts.customer ``` @@ -148,14 +208,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: customers } = useQueryGraphStep({ - entity: "customer", +const { data: carts } = useQueryGraphStep({ + entity: "cart", fields: [ - "carts.*", + "customer.*", ], }) -// customers.carts +// carts.customer ``` @@ -165,24 +225,24 @@ const { data: customers } = useQueryGraphStep({ ## Order Module -Medusa defines a read-only link between the `Customer` data model and the [Order Module](../../order/page.mdx)'s `Order` data model. This means you can retrieve the details of a customer's orders, but you don't manage the links in a pivot table in the database. The customer of an order is determined by the `customer_id` property of the `Order` data model. +Medusa defines a read-only link between the [Order Module](../../order/page.mdx)'s `Order` data model and the `Customer` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the customer of an order, and not the other way around. ### Retrieve with Query -To retrieve a customer's orders with [Query](!docs!/learn/fundamentals/module-links/query), pass `orders.*` in `fields`: +To retrieve the customer of an order with [Query](!docs!/learn/fundamentals/module-links/query), pass `customer.*` in `fields`: ```ts -const { data: customers } = await query.graph({ - entity: "customer", +const { data: orders } = await query.graph({ + entity: "order", fields: [ - "orders.*", + "customer.*", ], }) -// customers.orders +// orders.customer ``` @@ -193,14 +253,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: customers } = useQueryGraphStep({ - entity: "customer", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ - "orders.*", + "customer.*", ], }) -// customers.orders +// orders.customer ``` diff --git a/www/apps/resources/app/commerce-modules/fulfillment/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/fulfillment/links-to-other-modules/page.mdx index 97cf30cc66a68..c0d6f0983f08d 100644 --- a/www/apps/resources/app/commerce-modules/fulfillment/links-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/fulfillment/links-to-other-modules/page.mdx @@ -1,4 +1,4 @@ -import { CodeTabs, CodeTab } from "docs-ui" +import { CodeTabs, CodeTab, Table } from "docs-ui" export const metadata = { title: `Links between Fulfillment Module and Other Modules`, @@ -12,12 +12,110 @@ This document showcases the module links defined between the Fulfillment Module The Fulfillment Module has the following links to other modules: -- [`Order` data model of the Order Module \<\> `Fulfillment` data model](#order-module). -- [`Return` data model of the Order Module \<\> `Fulfillment` data model](#order-module). -- [`PriceSet` data model of the Pricing Module \<\> `ShippingOption` data model](#pricing-module). -- [`Product` data model of the Product Module \<\> `ShippingProfile` data model](#product-module). -- [`StockLocation` data model of the Stock Location Module \<\> `FulfillmentProvider` data model](#stock-location-module). -- [`StockLocation` data model of the Stock Location Module \<\> `FulfillmentSet` data model](#stock-location-module). + + + + + First Data Model + + + Second Data Model + + + Type + + + Description + + + + + + + [Order](/references/order/models/Order) in [Order Module](../../order/page.mdx) + + + [Fulfillment](/references/fulfillment/models/Fulfillment) + + + Stored + + + [Learn more](#order-module) + + + + + [Return](/references/order/models/Return) in [Order Module](../../order/page.mdx) + + + [Fulfillment](/references/fulfillment/models/Fulfillment) + + + Stored + + + [Learn more](#order-module) + + + + + [PriceSet](/references/pricing/models/PriceSet) in [Pricing Module](../../pricing/page.mdx) + + + [ShippingOption](/references/fulfillment/models/ShippingOption) + + + Stored + + + [Learn more](#pricing-module) + + + + + [Product](/references/product/models/Product) in [Product Module](../../product/page.mdx) + + + [ShippingProfile](/references/fulfillment/models/ShippingProfile) + + + Stored + + + [Learn more](#product-module) + + + + + [StockLocation](/references/stock-location/models/StockLocation) in [Stock Location Module](../../stock-location/page.mdx) + + + [FulfillmentProvider](/references/fulfillment/models/FulfillmentProvider) + + + Stored + + + [Learn more](#stock-location-module) + + + + + [StockLocation](/references/stock-location/models/StockLocation) in [Stock Location Module](../../stock-location/page.mdx) + + + [FulfillmentSet](/references/fulfillment/models/FulfillmentSet) + + + Stored + + + [Learn more](#stock-location-module) + + + +
--- diff --git a/www/apps/resources/app/commerce-modules/inventory/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/inventory/links-to-other-modules/page.mdx index bcb82e0f52e5f..f633b618c473c 100644 --- a/www/apps/resources/app/commerce-modules/inventory/links-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/inventory/links-to-other-modules/page.mdx @@ -1,4 +1,4 @@ -import { CodeTabs, CodeTab } from "docs-ui" +import { CodeTabs, CodeTab, Table } from "docs-ui" export const metadata = { title: `Links between Inventory Module and Other Modules`, @@ -18,8 +18,54 @@ Read-only links are used to query data across modules, but the relations aren't -- [`ProductVariant` data model of Product Module \<\> `InventoryItem` data model](#product-module). -- [`InventoryLevel` data model \<\> `StockLocation` data model of Stock Location Module](#stock-location-module). (Read-only). + + + + + First Data Model + + + Second Data Model + + + Type + + + Description + + + + + + + [ProductVariant](/references/product/models/ProductVariant) in [Product Module](../../product/page.mdx) + + + [InventoryItem](/references/inventory/models/InventoryItem) + + + Stored + + + [Learn more](#product-module) + + + + + [InventoryLevel](/references/inventory/models/InventoryLevel) + + + [StockLocation](/references/stock-location/models/StockLocation) in [Stock Location Module](../../stock-location/page.mdx) + + + Read-only + + + [Learn more](#stock-location-module) + + + +
--- diff --git a/www/apps/resources/app/commerce-modules/order/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/order/links-to-other-modules/page.mdx index bce80e0cf39c9..abc6b0744e1e3 100644 --- a/www/apps/resources/app/commerce-modules/order/links-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/order/links-to-other-modules/page.mdx @@ -1,4 +1,4 @@ -import { CodeTabs, CodeTab } from "docs-ui" +import { CodeTabs, CodeTab, Table } from "docs-ui" export const metadata = { title: `Links between Order Module and Other Modules`, @@ -18,17 +18,180 @@ Read-only links are used to query data across modules, but the relations aren't -- [`Order` data model \<\> `Customer` data model of Customer Module](#customer-module). (Read-only). -- [`Order` data model \<\> `Cart` data model of Cart Module](#cart-module). -- [`Order` data model \<\> `Fulfillment` data model of Fulfillment Module](#fulfillment-module). -- [`Return` data model \<\> `Fulfillment` data model of Fulfillment Module](#fulfillment-module). -- [`Order` data model \<\> `PaymentCollection` data model of Payment Module](#payment-module). -- [`OrderClaim` data model \<\> `PaymentCollection` data model of Payment Module](#payment-module). -- [`OrderExchange` data model \<\> `PaymentCollection` data model of Payment Module](#payment-module). -- [`Order` data model \<\> `Product` data model of Product Module](#product-module). (Read-only). -- [`Order` data model \<\> `Promotion` data model of Promotion Module](#promotion-module). -- [`Order` data model \<\> `Region` data model of Region Module](#region-module). (Read-only). -- [`Order` data model \<\> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). (Read-only). + + + + + First Data Model + + + Second Data Model + + + Type + + + Description + + + + + + + [Order](/references/order/models/Order) + + + [Customer](/references/customer/models/Customer) in [Customer Module](../../customer/page.mdx) + + + Read-only + + + [Learn more](#customer-module) + + + + + [Order](/references/order/models/Order) + + + [Cart](/references/cart/models/Cart) in [Cart Module](../../cart/page.mdx) + + + Stored + + + [Learn more](#cart-module) + + + + + [Order](/references/order/models/Order) + + + [Fulfillment](/references/fulfillment/models/Fulfillment) in [Fulfillment Module](../../fulfillment/page.mdx) + + + Stored + + + [Learn more](#fulfillment-module) + + + + + [Return](/references/order/models/Return) + + + [Fulfillment](/references/fulfillment/models/Fulfillment) in [Fulfillment Module](../../fulfillment/page.mdx) + + + Stored + + + [Learn more](#fulfillment-module) + + + + + [Order](/references/order/models/Order) + + + [PaymentCollection](/references/payment/models/PaymentCollection) in [Payment Module](../../payment/page.mdx) + + + Stored + + + [Learn more](#payment-module) + + + + + [OrderClaim](/references/order/models/OrderClaim) + + + [PaymentCollection](/references/payment/models/PaymentCollection) in [Payment Module](../../payment/page.mdx) + + + Stored + + + [Learn more](#payment-module) + + + + + [OrderExchange](/references/order/models/OrderExchange) + + + [PaymentCollection](/references/payment/models/PaymentCollection) in [Payment Module](../../payment/page.mdx) + + + Stored + + + [Learn more](#payment-module) + + + + + [Order](/references/order/models/Order) + + + [Product](/references/product/models/Product) in [Product Module](../../product/page.mdx) + + + Read-only + + + [Learn more](#product-module) + + + + + [Order](/references/order/models/Order) + + + [Promotion](/references/promotion/models/Promotion) in [Promotion Module](../../promotion/page.mdx) + + + Stored + + + [Learn more](#promotion-module) + + + + + [Order](/references/order/models/Order) + + + [Region](/references/region/models/Region) in [Region Module](../../region/page.mdx) + + + Read-only + + + [Learn more](#region-module) + + + + + [Order](/references/order/models/Order) + + + [SalesChannel](/references/sales-channel/models/SalesChannel) in [Sales Channel Module](../../sales-channel/page.mdx) + + + Read-only + + + [Learn more](#sales-channel-module) + + + +
--- diff --git a/www/apps/resources/app/commerce-modules/payment/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/payment/links-to-other-modules/page.mdx index 649f423133db0..67cd9c78d534f 100644 --- a/www/apps/resources/app/commerce-modules/payment/links-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/payment/links-to-other-modules/page.mdx @@ -1,4 +1,4 @@ -import { CodeTabs, CodeTab } from "docs-ui" +import { CodeTabs, CodeTab, Table } from "docs-ui" export const metadata = { title: `Links between Payment Module and Other Modules`, @@ -12,12 +12,110 @@ This document showcases the module links defined between the Payment Module and The Payment Module has the following links to other modules: -- [`Cart` data model of Cart Module \<\> `PaymentCollection` data model](#cart-module). -- [`Customer` data model of Customer Module \<\> `AccountHolder` data model](#customer-module). -- [`Order` data model of Order Module \<\> `PaymentCollection` data model](#order-module). -- [`OrderClaim` data model of Order Module \<\> `PaymentCollection` data model](#order-module). -- [`OrderExchange` data model of Order Module \<\> `PaymentCollection` data model](#order-module). -- [`Region` data model of Region Module \<\> `PaymentProvider` data model](#region-module). + + + + + First Data Model + + + Second Data Model + + + Type + + + Description + + + + + + + [Cart](/references/cart/models/Cart) in [Cart Module](../../cart/page.mdx) + + + [PaymentCollection](/references/payment/models/PaymentCollection) + + + Stored + + + [Learn more](#cart-module) + + + + + [Customer](/references/customer/models/Customer) in [Customer Module](../../customer/page.mdx) + + + [AccountHolder](/references/payment/models/AccountHolder) + + + Stored + + + [Learn more](#customer-module) + + + + + [Order](/references/order/models/Order) in [Order Module](../../order/page.mdx) + + + [PaymentCollection](/references/payment/models/PaymentCollection) + + + Stored + + + [Learn more](#order-module) + + + + + [OrderClaim](/references/order/models/OrderClaim) in [Order Module](../../order/page.mdx) + + + [PaymentCollection](/references/payment/models/PaymentCollection) + + + Stored + + + [Learn more](#order-module) + + + + + [OrderExchange](/references/order/models/OrderExchange) in [Order Module](../../order/page.mdx) + + + [PaymentCollection](/references/payment/models/PaymentCollection) + + + Stored + + + [Learn more](#order-module) + + + + + [Region](/references/region/models/Region) in [Region Module](../../region/page.mdx) + + + [PaymentProvider](/references/payment/models/PaymentProvider) + + + Stored + + + [Learn more](#region-module) + + + +
--- diff --git a/www/apps/resources/app/commerce-modules/pricing/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/pricing/links-to-other-modules/page.mdx index af41b44cd86d1..d530e62be8286 100644 --- a/www/apps/resources/app/commerce-modules/pricing/links-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/pricing/links-to-other-modules/page.mdx @@ -1,4 +1,4 @@ -import { CodeTabs, CodeTab } from "docs-ui" +import { CodeTabs, CodeTab, Table } from "docs-ui" export const metadata = { title: `Links between Pricing Module and Other Modules`, @@ -12,8 +12,54 @@ This document showcases the module links defined between the Pricing Module and The Pricing Module has the following links to other modules: -- [`ShippingOption` data model of Fulfillment Module \<\> `PriceSet` data model](#fulfillment-module). -- [`ProductVariant` data model of Product Module \<\> `PriceSet` data model](#product-module). + + + + + First Data Model + + + Second Data Model + + + Type + + + Description + + + + + + + [ShippingOption](/references/fulfillment/models/ShippingOption) in [Fulfillment Module](../../fulfillment/page.mdx) + + + [PriceSet](/references/pricing/models/PriceSet) + + + Stored + + + [Learn more](#fulfillment-module) + + + + + [ProductVariant](/references/product/models/ProductVariant) in [Product Module](../../product/page.mdx) + + + [PriceSet](/references/pricing/models/PriceSet) + + + Stored + + + [Learn more](#product-module) + + + +
--- diff --git a/www/apps/resources/app/commerce-modules/product/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/product/links-to-other-modules/page.mdx index 8584b4f55b97b..a4a27ca048acb 100644 --- a/www/apps/resources/app/commerce-modules/product/links-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/product/links-to-other-modules/page.mdx @@ -1,4 +1,4 @@ -import { CodeTabs, CodeTab } from "docs-ui" +import { CodeTabs, CodeTab, Table } from "docs-ui" export const metadata = { title: `Links between Product Module and Other Modules`, @@ -18,12 +18,110 @@ Read-only links are used to query data across modules, but the relations aren't -- [`Product` data model \<\> `Cart` data model of Cart Module](#cart-module). (Read-only). -- [`Product` data model \<\> `ShippingProfile` data model of Fulfillment Module](#fulfillment-module). -- [`ProductVariant` data model \<\> `InventoryItem` data model of Inventory Module](#inventory-module). -- [`Product` data model \<\> `Order` data model of Order Module](#order-module). (Read-only). -- [`ProductVariant` data model \<\> `PriceSet` data model of Pricing Module](#pricing-module). -- [`Product` data model \<\> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). + + + + + First Data Model + + + Second Data Model + + + Type + + + Description + + + + + + + [Cart](/references/cart/models/Cart) in [Cart Module](../../cart/page.mdx) + + + [Product](/references/product/models/Product) + + + Read-only + + + [Learn more](#cart-module) + + + + + [Product](/references/product/models/Product) + + + [ShippingProfile](/references/fulfillment/models/ShippingProfile) in [Fulfillment Module](../../fulfillment/page.mdx) + + + Stored + + + [Learn more](#fulfillment-module) + + + + + [ProductVariant](/references/product/models/ProductVariant) + + + [InventoryItem](/references/inventory/models/InventoryItem) in [Inventory Module](../../inventory/page.mdx) + + + Stored + + + [Learn more](#inventory-module) + + + + + [Order](/references/order/models/Order) in [Order Module](../../order/page.mdx) + + + [Product](/references/product/models/Product) + + + Read-only + + + [Learn more](#order-module) + + + + + [ProductVariant](/references/product/models/ProductVariant) + + + [PriceSet](/references/pricing/models/PriceSet) in [Pricing Module](../../pricing/page.mdx) + + + Stored + + + [Learn more](#pricing-module) + + + + + [Product](/references/product/models/Product) + + + [SalesChannel](/references/sales-channel/models/SalesChannel) in [Sales Channel Module](../../sales-channel/page.mdx) + + + Stored + + + [Learn more](#sales-channel-module) + + + +
--- @@ -31,16 +129,16 @@ Read-only links are used to query data across modules, but the relations aren't Medusa defines read-only links between: -- The `Product` data model and the [Cart Module](../../cart/page.mdx)'s `LineItem` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `LineItem` data model. -- The `ProductVariant` data model and the [Cart Module](../../cart/page.mdx)'s `LineItem` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `LineItem` data model. +- The [Cart Module](../../cart/page.mdx)'s `LineItem` data model and the `Product` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the product of a line item, and not the other way around. +- The `ProductVariant` data model and the [Cart Module](../../cart/page.mdx)'s `LineItem` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the variant of a line item, and not the other way around. ### Retrieve with Query -To retrieve the line items of a variant with [Query](!docs!/learn/fundamentals/module-links/query), pass `line_items.*` in `fields`: +To retrieve the variant of a line item with [Query](!docs!/learn/fundamentals/module-links/query), pass `variant.*` in `fields`: -To retrieve the line items of a product, pass `line_items.*` in `fields`. +To retrieve the product, pass `product.*` in `fields`. @@ -48,14 +146,14 @@ To retrieve the line items of a product, pass `line_items.*` in `fields`. ```ts -const { data: variants } = await query.graph({ - entity: "variant", +const { data: lineItems } = await query.graph({ + entity: "line_item", fields: [ - "line_items.*", + "variant.*", ], }) -// variants.line_items +// lineItems.variant ``` @@ -66,14 +164,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: variants } = useQueryGraphStep({ - entity: "variant", +const { data: lineItems } = useQueryGraphStep({ + entity: "line_item", fields: [ - "line_items.*", + "variant.*", ], }) -// variants.line_items +// lineItems.variant ``` @@ -281,16 +379,16 @@ createRemoteLinkStep({ Medusa defines read-only links between: -- the `Product` data model and the [Order Module](../../order/page.mdx)'s `OrderLineItem` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `OrderLineItem` data model. -- the `ProductVariant` data model and the [Order Module](../../order/page.mdx)'s `OrderLineItem` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `OrderLineItem` data model. +- the [Order Module](../../order/page.mdx)'s `OrderLineItem` data model and the `Product` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the product of an order line item, and not the other way around. +- the [Order Module](../../order/page.mdx)'s `OrderLineItem` data model and the `ProductVariant` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the variant of an order line item, and not the other way around. ### Retrieve with Query -To retrieve the order line items of a variant with [Query](!docs!/learn/fundamentals/module-links/query), pass `order_items.*` in `fields`: +To retrieve the variant of a line item with [Query](!docs!/learn/fundamentals/module-links/query), pass `variant.*` in `fields`: -To retrieve a product's order line items, pass `order_items.*` in `fields`. +To retrieve the product, pass `product.*` in `fields`. @@ -298,14 +396,14 @@ To retrieve a product's order line items, pass `order_items.*` in `fields`. ```ts -const { data: variants } = await query.graph({ - entity: "variant", +const { data: lineItems } = await query.graph({ + entity: "order_line_item", fields: [ - "order_items.*", + "variant.*", ], }) -// variants.order_items +// lineItems.variant ``` @@ -316,14 +414,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: variants } = useQueryGraphStep({ - entity: "variant", +const { data: lineItems } = useQueryGraphStep({ + entity: "order_line_item", fields: [ - "order_items.*", + "variant.*", ], }) -// variants.order_items +// lineItems.variant ``` diff --git a/www/apps/resources/app/commerce-modules/promotion/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/promotion/links-to-other-modules/page.mdx index 183de7ea8d34b..fdddd1e86aa4c 100644 --- a/www/apps/resources/app/commerce-modules/promotion/links-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/promotion/links-to-other-modules/page.mdx @@ -1,4 +1,4 @@ -import { CodeTabs, CodeTab } from "docs-ui" +import { CodeTabs, CodeTab, Table } from "docs-ui" export const metadata = { title: `Links between Promotion Module and Other Modules`, @@ -18,9 +18,68 @@ Read-only links are used to query data across modules, but the relations aren't -- [`Cart` data model of the Cart Module \<\> `Promotion` data model](#cart-module). -- [`LineItemAdjustment` data model of the Cart Module \<\> `Promotion` data model](#cart-module). (Read-only). -- [`Order` data model of the Order Module \<\> `Promotion` data model](#order-module). + + + + + First Data Model + + + Second Data Model + + + Type + + + Description + + + + + + + [Cart](/references/cart/models/Cart) in [Cart Module](../../cart/page.mdx) + + + [Promotion](/references/promotion/models/Promotion) + + + Stored + + + [Learn more](#cart-module) + + + + + [LineItemAdjustment](/references/cart/models/LineItemAdjustment) in [Cart Module](../../cart/page.mdx) + + + [Promotion](/references/promotion/models/Promotion) + + + Read-only + + + [Learn more](#cart-module) + + + + + [Order](/references/order/models/Order) in [Order Module](../../order/page.mdx) + + + [Promotion](/references/promotion/models/Promotion) + + + Stored + + + [Learn more](#order-module) + + + +
--- @@ -30,7 +89,7 @@ A promotion can be applied on line items and shipping methods of a cart. Medusa ![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) -Medusa also defines a read-only link between the `Promotion` data model and the [Cart Module](../../cart/page.mdx)'s `LineItemAdjustment` data model. This means you can retrieve the details of the promotion applied on a line item, but you don't manage the links in a pivot table in the database. The promotion of a line item is determined by the `promotion_id` property of the `LineItemAdjustment` data model. +Medusa also defines a read-only link between the [Cart Module](../../cart/page.mdx)'s `LineItemAdjustment` data model and the `Promotion` data model. Because the link is read-only from the `LineItemAdjustment`'s side, you can only retrieve the promotion applied on a line item, and not the other way around. ### Retrieve with Query @@ -38,7 +97,7 @@ To retrieve the carts that a promotion is applied on with [Query](!docs!/learn/f -To retrieve the line item adjustments of a promotion, pass `line_item_adjustments.*` in `fields`. +To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. diff --git a/www/apps/resources/app/commerce-modules/region/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/region/links-to-other-modules/page.mdx index a6f1515a13400..12d4bf8cfc16b 100644 --- a/www/apps/resources/app/commerce-modules/region/links-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/region/links-to-other-modules/page.mdx @@ -1,4 +1,4 @@ -import { CodeTabs, CodeTab } from "docs-ui" +import { CodeTabs, CodeTab, Table } from "docs-ui" export const metadata = { title: `Links between Region Module and Other Modules`, @@ -18,32 +18,91 @@ Read-only links are used to query data across modules, but the relations aren't -- [`Region` data model \<\> `Cart` data model of the Cart Module](#cart-module). (Read-only) -- [`Region` data model \<\> `Order` data model of the Order Module](#order-module). (Read-only) -- [`Region` data model \<\> `PaymentProvider` data model of the Payment Module](#payment-module). + + + + + First Data Model + + + Second Data Model + + + Type + + + Description + + + + + + + [Cart](/references/cart/models/Cart) in [Cart Module](../../cart/page.mdx) + + + [Region](/references/region/models/Region) + + + Read-only + + + [Learn more](#cart-module) + + + + + [Order](/references/order/models/Order) in [Order Module](../../order/page.mdx) + + + [Region](/references/region/models/Region) + + + Read-only + + + [Learn more](#order-module) + + + + + [Region](/references/region/models/Region) + + + [PaymentProvider](/references/payment/models/PaymentProvider) in [Payment Module](../../payment/page.mdx) + + + Stored + + + [Learn more](#payment-module) + + + +
--- ## Cart Module -Medusa defines a read-only link between the `Region` data model and the [Cart Module](../../cart/page.mdx)'s `Cart` data model. This means you can retrieve the details of a region's carts, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. +Medusa defines a read-only link between the [Cart Module](../../cart/page.mdx)'s `Cart` data model and the `Region` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the region of a cart, and not the other way around. ### Retrieve with Query -To retrieve the carts of a region with [Query](!docs!/learn/fundamentals/module-links/query), pass `carts.*` in `fields`: +To retrieve the region of a cart with [Query](!docs!/learn/fundamentals/module-links/query), pass `region.*` in `fields`: ```ts -const { data: regions } = await query.graph({ - entity: "region", +const { data: carts } = await query.graph({ + entity: "cart", fields: [ - "carts.*", + "region.*", ], }) -// regions.carts +// carts.region ``` @@ -54,14 +113,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: regions } = useQueryGraphStep({ - entity: "region", +const { data: carts } = useQueryGraphStep({ + entity: "cart", fields: [ - "carts.*", + "region.*", ], }) -// regions.carts +// carts.region ``` @@ -71,24 +130,24 @@ const { data: regions } = useQueryGraphStep({ ## Order Module -Medusa defines a read-only link between the `Region` data model and the [Cart Module](../../cart/page.mdx)'s `Cart` data model. This means you can retrieve the details of a region's orders, but you don't manage the links in a pivot table in the database. The region of an order is determined by the `region_id` property of the `Order` data model. +Medusa defines a read-only link between the [Order Module](../../order/page.mdx)'s `Order` data model and the `Region` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the region of an order, and not the other way around. ### Retrieve with Query -To retrieve the orders of a region with [Query](!docs!/learn/fundamentals/module-links/query), pass `orders.*` in `fields`: +To retrieve the region of an order with [Query](!docs!/learn/fundamentals/module-links/query), pass `region.*` in `fields`: ```ts -const { data: regions } = await query.graph({ - entity: "region", +const { data: orders } = await query.graph({ + entity: "order", fields: [ - "orders.*", + "region.*", ], }) -// regions.orders +// orders.region ``` @@ -99,14 +158,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: regions } = useQueryGraphStep({ - entity: "region", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ - "orders.*", + "region.*", ], }) -// regions.orders +// orders.region ``` diff --git a/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/page.mdx index 6709748e2ee45..02adf1e836a54 100644 --- a/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/page.mdx @@ -1,4 +1,4 @@ -import { CodeTabs, CodeTab } from "docs-ui" +import { CodeTabs, CodeTab, Table } from "docs-ui" export const metadata = { title: `Links between Sales Channel Module and Other Modules`, @@ -18,11 +18,96 @@ Read-only links are used to query data across modules, but the relations aren't -- [`ApiKey` data model of the API Key Module \<\> `SalesChannel` data model](#api-key-module). -- [`SalesChannel` data model \<\> `Cart` data model of the Cart Module](#cart-module). (Read-only) -- [`SalesChannel` data model \<\> `Order` data model of the Order Module](#order-module). (Read-only) -- [`Product` data model of the Product Module \<\> `SalesChannel` data model](#product-module). -- [`SalesChannel` data model \<\> `StockLocation` data model of the Stock Location Module](#stock-location-module). + + + + + First Data Model + + + Second Data Model + + + Type + + + Description + + + + + + + [ApiKey](/references/api-key/models/ApiKey) in [API Key Module](../../api-key/page.mdx) + + + [SalesChannel](/references/sales-channel/models/SalesChannel) + + + Stored + + + [Learn more](#api-key-module) + + + + + [Cart](/references/cart/models/Cart) in [Cart Module](../../cart/page.mdx) + + + [SalesChannel](/references/sales-channel/models/SalesChannel) + + + Read-only + + + [Learn more](#cart-module) + + + + + [Order](/references/order/models/Order) in [Order Module](../../order/page.mdx) + + + [SalesChannel](/references/sales-channel/models/SalesChannel) + + + Read-only + + + [Learn more](#order-module) + + + + + [Product](/references/product/models/Product) in [Product Module](../../product/page.mdx) + + + [SalesChannel](/references/sales-channel/models/SalesChannel) + + + Stored + + + [Learn more](#product-module) + + + + + [SalesChannel](/references/sales-channel/models/SalesChannel) + + + [StockLocation](/references/stock-location/models/StockLocation) in [Stock Location Module](../../stock-location/page.mdx) + + + Stored + + + [Learn more](#stock-location-module) + + + +
--- @@ -121,24 +206,24 @@ createRemoteLinkStep({ ## Cart Module -Medusa defines a read-only link between the `SalesChannel` data model and the [Cart Module](../../cart/page.mdx)'s `Cart` data model. This means you can retrieve the details of a sales channel's carts, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. +Medusa defines a read-only link between the [Cart Module](../../cart/page.mdx)'s `Cart` data model and the `SalesChannel` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the sales channel of a cart, and not the other way around. ### Retrieve with Query -To retrieve the carts of a sales channel with [Query](!docs!/learn/fundamentals/module-links/query), pass `carts.*` in `fields`: +To retrieve the sales channel of a cart with [Query](!docs!/learn/fundamentals/module-links/query), pass `sales_channel.*` in `fields`: ```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", +const { data: carts } = await query.graph({ + entity: "cart", fields: [ - "carts.*", + "sales_channel.*", ], }) -// salesChannels.carts +// carts.sales_channel ``` @@ -149,14 +234,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", +const { data: carts } = useQueryGraphStep({ + entity: "cart", fields: [ - "carts.*", + "sales_channel.*", ], }) -// salesChannels.carts +// carts.sales_channel ``` @@ -166,24 +251,24 @@ const { data: salesChannels } = useQueryGraphStep({ ## Order Module -Medusa defines a read-only link between the `SalesChannel` data model and the [Order Module](../../order/page.mdx)'s `Order` data model. This means you can retrieve the details of a sales channel's orders, but you don't manage the links in a pivot table in the database. The sales channel of an order is determined by the `sales_channel_id` property of the `Order` data model. +Medusa defines a read-only link between the [Order Module](../../order/page.mdx)'s `Order` data model and the `SalesChannel` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the sales channel of an order, and not the other way around. ### Retrieve with Query -To retrieve the orders of a sales channel with [Query](!docs!/learn/fundamentals/module-links/query), pass `orders.*` in `fields`: +To retrieve the sales channel of an order with [Query](!docs!/learn/fundamentals/module-links/query), pass `sales_channel.*` in `fields`: ```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", +const { data: orders } = await query.graph({ + entity: "order", fields: [ - "orders.*", + "sales_channel.*", ], }) -// salesChannels.orders +// orders.sales_channel ``` @@ -194,14 +279,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ - "orders.*", + "sales_channel.*", ], }) -// salesChannels.orders +// orders.sales_channel ``` diff --git a/www/apps/resources/app/commerce-modules/stock-location/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/stock-location/links-to-other-modules/page.mdx index 6cd0369f2d7c1..b1bc926412460 100644 --- a/www/apps/resources/app/commerce-modules/stock-location/links-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/stock-location/links-to-other-modules/page.mdx @@ -1,4 +1,4 @@ -import { CodeTabs, CodeTab } from "docs-ui" +import { CodeTabs, CodeTab, Table } from "docs-ui" export const metadata = { title: `Links between Stock Location Module and Other Modules`, @@ -18,10 +18,82 @@ Read-only links are used to query data across modules, but the relations aren't -- [`FulfillmentSet` data model of the Fulfillment Module \<\> `StockLocation` data model](#fulfillment-module). -- [`FulfillmentProvider` data model of the Fulfillment Module \<\> `StockLocation` data model](#fulfillment-module). -- [`StockLocation` data model \<\> `Inventory` data model of the Inventory Module](#inventory-module). -- [`SalesChannel` data model of the Sales Channel Module \<\> `StockLocation` data model](#sales-channel-module). + + + + + First Data Model + + + Second Data Model + + + Type + + + Description + + + + + + + [FulfillmentSet](/references/fulfillment/models/FulfillmentSet) in [Fulfillment Module](../../fulfillment/page.mdx) + + + [StockLocation](/references/stock-location/models/StockLocation) + + + Stored + + + [Learn more](#fulfillment-module) + + + + + [FulfillmentProvider](/references/fulfillment/models/FulfillmentProvider) in [Fulfillment Module](../../fulfillment/page.mdx) + + + [StockLocation](/references/stock-location/models/StockLocation) + + + Stored + + + [Learn more](#fulfillment-module) + + + + + [Inventory](/references/inventory/models/Inventory) in [Inventory Module](../../inventory/page.mdx) + + + [StockLocation](/references/stock-location/models/StockLocation) + + + Stored + + + [Learn more](#inventory-module) + + + + + [SalesChannel](/references/sales-channel/models/SalesChannel) in [Sales Channel Module](../../sales-channel/page.mdx) + + + [StockLocation](/references/stock-location/models/StockLocation) + + + Stored + + + [Learn more](#sales-channel-module) + + + +
--- @@ -130,24 +202,24 @@ createRemoteLinkStep({ ## Inventory Module -Medusa defines a read-only link between the `StockLocation` data model and the [Inventory Module](../../inventory/page.mdx)'s `InventoryLevel` data model. This means you can retrieve the details of a stock location's inventory levels, but you don't manage the links in a pivot table in the database. The stock location of an inventory level is determined by the `location_id` property of the `InventoryLevel` data model. +Medusa defines a read-only link between the [Inventory Module](../../inventory/page.mdx)'s `InventoryLevel` data model and the `StockLocation` data model. Because the link is read-only from the `InventoryLevel`'s side, you can only retrieve the stock location of an inventory level, and not the other way around. ### Retrieve with Query -To retrieve the inventory levels of a stock location with [Query](!docs!/learn/fundamentals/module-links/query), pass `inventory_levels.*` in `fields`: +To retrieve the stock locations of an inventory level with [Query](!docs!/learn/fundamentals/module-links/query), pass `stock_locations.*` in `fields`: ```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", +const { data: inventoryLevels } = await query.graph({ + entity: "inventory_level", fields: [ - "inventory_levels.*", + "stock_locations.*", ], }) -// stockLocations.inventory_levels +// inventoryLevels.stock_locations ``` @@ -158,14 +230,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", +const { data: inventoryLevels } = useQueryGraphStep({ + entity: "inventory_level", fields: [ - "inventory_levels.*", + "stock_locations.*", ], }) -// stockLocations.inventory_levels +// inventoryLevels.stock_locations ``` diff --git a/www/apps/resources/app/commerce-modules/store/links-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/store/links-to-other-modules/page.mdx index 10132231b342b..cd6d21b538ca4 100644 --- a/www/apps/resources/app/commerce-modules/store/links-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/store/links-to-other-modules/page.mdx @@ -1,4 +1,4 @@ -import { CodeTabs, CodeTab } from "docs-ui" +import { CodeTabs, CodeTab, Table } from "docs-ui" export const metadata = { title: `Links between Store Module and Other Modules`, @@ -18,7 +18,40 @@ Read-only links are used to query data across modules, but the relations aren't -- [`Currency` data model \<\> `Currency` data model of Currency Module](#currency-module). (Read-only). + + + + + First Data Model + + + Second Data Model + + + Type + + + Description + + + + + + + [Store](/references/store/models/Store) + + + [Currency](/references/currency/models/Currency) in [Currency Module](../../currency/page.mdx) + + + Read-only + + + [Learn more](#currency-module) + + + +
--- @@ -26,7 +59,7 @@ Read-only links are used to query data across modules, but the relations aren't The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. -Instead, Medusa defines a read-only link between the [Currency Module](../../currency/page.mdx)'s `Currency` data model and the Store Module's `Currency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database. The currencies of a store are determined by the `currency_code` of the `Currency` data model in the Store Module. +Instead, Medusa defines a read-only link between the [Currency Module](../../currency/page.mdx)'s `Currency` data model and the Store Module's `Currency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database. The currencies of a store are determined by the `currency_code` of the [Currency](/references/store/models/Currency) data model in the Store Module (not in the Currency Module). ### Retrieve with Query diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index c407f54810d8b..5674aadeed7a2 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -2107,21 +2107,21 @@ export const generatedEditDates = { "app/admin-components/components/forms/page.mdx": "2024-10-09T12:48:04.229Z", "app/commerce-modules/auth/reset-password/page.mdx": "2025-02-26T11:18:00.391Z", "app/storefront-development/customers/reset-password/page.mdx": "2025-03-04T09:15:25.662Z", - "app/commerce-modules/api-key/links-to-other-modules/page.mdx": "2025-01-06T11:19:22.450Z", + "app/commerce-modules/api-key/links-to-other-modules/page.mdx": "2025-03-14T14:33:38.886Z", "app/commerce-modules/cart/extend/page.mdx": "2024-12-25T12:48:59.149Z", - "app/commerce-modules/cart/links-to-other-modules/page.mdx": "2025-01-06T11:19:35.593Z", + "app/commerce-modules/cart/links-to-other-modules/page.mdx": "2025-03-14T14:33:26.754Z", "app/commerce-modules/customer/extend/page.mdx": "2024-12-25T15:54:37.789Z", - "app/commerce-modules/fulfillment/links-to-other-modules/page.mdx": "2025-02-11T12:10:04.901Z", - "app/commerce-modules/inventory/links-to-other-modules/page.mdx": "2025-02-03T12:37:33.622Z", - "app/commerce-modules/pricing/links-to-other-modules/page.mdx": "2025-01-06T11:19:35.607Z", + "app/commerce-modules/fulfillment/links-to-other-modules/page.mdx": "2025-03-14T14:37:12.681Z", + "app/commerce-modules/inventory/links-to-other-modules/page.mdx": "2025-03-14T14:38:15.246Z", + "app/commerce-modules/pricing/links-to-other-modules/page.mdx": "2025-03-14T14:41:14.683Z", "app/commerce-modules/product/extend/page.mdx": "2024-12-11T09:07:25.252Z", - "app/commerce-modules/product/links-to-other-modules/page.mdx": "2025-02-11T12:09:46.420Z", + "app/commerce-modules/product/links-to-other-modules/page.mdx": "2025-03-14T15:07:30.349Z", "app/commerce-modules/promotion/extend/page.mdx": "2024-12-11T09:07:24.137Z", - "app/commerce-modules/promotion/links-to-other-modules/page.mdx": "2025-01-06T11:19:35.608Z", + "app/commerce-modules/promotion/links-to-other-modules/page.mdx": "2025-03-17T06:48:23.706Z", "app/commerce-modules/order/edit/page.mdx": "2025-02-26T11:24:28.852Z", - "app/commerce-modules/order/links-to-other-modules/page.mdx": "2025-01-06T11:19:35.604Z", + "app/commerce-modules/order/links-to-other-modules/page.mdx": "2025-03-14T14:39:37.366Z", "app/commerce-modules/order/order-change/page.mdx": "2024-10-09T09:59:40.745Z", - "app/commerce-modules/payment/links-to-other-modules/page.mdx": "2025-01-31T09:22:05.326Z", + "app/commerce-modules/payment/links-to-other-modules/page.mdx": "2025-03-14T14:40:25.733Z", "references/core_flows/Common/Steps_Common/functions/core_flows.Common.Steps_Common.useQueryGraphStep/page.mdx": "2025-01-13T17:30:23.157Z", "references/core_flows/Payment/Workflows_Payment/functions/core_flows.Payment.Workflows_Payment.processPaymentWorkflow/page.mdx": "2025-03-04T13:33:44.884Z", "references/helper_steps/functions/helper_steps.useQueryGraphStep/page.mdx": "2025-01-17T16:43:22.242Z", @@ -2169,10 +2169,10 @@ export const generatedEditDates = { "references/fulfillment/interfaces/fulfillment.IFulfillmentModuleService/page.mdx": "2024-12-17T16:57:25.097Z", "references/types/CommonTypes/interfaces/types.CommonTypes.RequestQueryFields/page.mdx": "2024-12-09T13:21:32.865Z", "references/utils/utils.ProductUtils/page.mdx": "2025-02-24T10:48:43.141Z", - "app/commerce-modules/region/links-to-other-modules/page.mdx": "2025-01-06T11:19:35.617Z", - "app/commerce-modules/sales-channel/links-to-other-modules/page.mdx": "2025-01-06T11:19:35.620Z", - "app/commerce-modules/stock-location/links-to-other-modules/page.mdx": "2025-01-06T11:19:35.619Z", - "app/commerce-modules/store/links-to-other-modules/page.mdx": "2024-12-24T14:58:24.038Z", + "app/commerce-modules/region/links-to-other-modules/page.mdx": "2025-03-14T15:08:31.803Z", + "app/commerce-modules/sales-channel/links-to-other-modules/page.mdx": "2025-03-14T15:09:08.416Z", + "app/commerce-modules/stock-location/links-to-other-modules/page.mdx": "2025-03-14T15:04:43.112Z", + "app/commerce-modules/store/links-to-other-modules/page.mdx": "2025-03-17T06:52:04.187Z", "app/examples/page.mdx": "2025-02-04T07:36:39.956Z", "app/medusa-cli/commands/build/page.mdx": "2024-11-11T11:00:49.665Z", "app/js-sdk/page.mdx": "2025-02-05T09:12:11.479Z", @@ -5726,8 +5726,8 @@ export const generatedEditDates = { "app/commerce-modules/store/admin-widget-zones/page.mdx": "2024-12-24T08:46:28.646Z", "app/commerce-modules/tax/admin-widget-zones/page.mdx": "2024-12-24T08:47:13.176Z", "app/commerce-modules/user/admin-widget-zones/page.mdx": "2024-12-24T08:48:14.186Z", - "app/commerce-modules/currency/links-to-other-modules/page.mdx": "2024-12-24T14:47:10.556Z", - "app/commerce-modules/customer/links-to-other-modules/page.mdx": "2025-01-31T09:26:54.541Z", + "app/commerce-modules/currency/links-to-other-modules/page.mdx": "2025-03-17T06:41:29.161Z", + "app/commerce-modules/customer/links-to-other-modules/page.mdx": "2025-03-14T15:18:39.759Z", "app/commerce-modules/fulfillment/events/page.mdx": "2024-12-31T09:37:49.253Z", "app/commerce-modules/payment/events/page.mdx": "2024-12-31T09:41:56.582Z", "references/core_flows/Payment/Steps_Payment/functions/core_flows.Payment.Steps_Payment.refundPaymentsStep/page.mdx": "2025-03-04T13:33:44.866Z",