From 01eaa0cc9d030bb636a3410a541d30030e73800a Mon Sep 17 00:00:00 2001 From: Shahed Nasser <shahednasser@gmail.com> Date: Tue, 4 Mar 2025 11:51:04 +0200 Subject: [PATCH 1/4] docs: changes for next release --- .../api-routes/middlewares/page.mdx | 221 +- www/apps/book/generated/edit-dates.mjs | 2 +- www/apps/book/public/llms-full.txt | 25856 ++++++++-------- .../auth/authentication-route/page.mdx | 13 +- .../customers/reset-password/page.mdx | 14 +- www/apps/resources/generated/edit-dates.mjs | 4 +- 6 files changed, 13205 insertions(+), 12905 deletions(-) diff --git a/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx index 7d69c2eb4ee52..4018265cf07f4 100644 --- a/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx +++ b/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx @@ -18,11 +18,20 @@ As Medusa's server is based on Express, you can use any [Express middleware](htt </Note> +### Middleware Types + +There are two types of middlewares: + +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). + +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. + --- -## How to Create a Middleware? +## How to Create a Global Middleware? -Middlewares 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. +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. For example: @@ -59,11 +68,67 @@ The `defineMiddlewares` function accepts a middleware configurations object that - `matcher`: a string or regular expression indicating the API route path to apply the middleware on. The regular expression must be compatible with [path-to-regexp](https://github.com/pillarjs/path-to-regexp). - `middlewares`: An array of middleware functions. -In the example above, you define a middleware that logs the message `Received a request!` whenever a request is sent to an API route path starting with `/custom`. +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 + +To test the middleware: + +1. Start the application: + +```bash npm2yarn +npm run dev +``` + +2. Send a request to any API route starting with `/custom`. +3. See the following message in the terminal: + +```bash +Received a request! +``` --- -## Test the Middleware +## How to Create a Route Middleware? + +In the previous section, you learned how to create a global middleware. You define the route middleware in the same way, but you specify an additional property `method`. Its value is one or more HTTP methods to apply the middleware to. + +For example: + +export const highlights = [["12", "method", "Apply the middleware only on `POST` requests"]] + +```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, + defineMiddlewares, +} 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!") + + next() + }, + ], + }, + ], +}) +``` + +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 To test the middleware: @@ -73,7 +138,7 @@ To test the middleware: npm run dev ``` -2. Send a request to any API route starting with `/custom`. +2. Send a `POST` request to any API route starting with `/custom`. 3. See the following message in the terminal: ```bash @@ -141,15 +206,13 @@ This applies a middleware to the routes defined in the file `src/api/custom/[id] --- -## Restrict HTTP Methods - -Restrict which HTTP methods the middleware is applied to using the `method` property of the middleware route object. +## Request URLs with Trailing Backslashes -For example: +A middleware whose `matcher` pattern doesn't end with a backslash won't be applied for requests to URLs with a trailing backslash. -export const highlights = [["12", "method", "Apply the middleware only on `POST` requests"]] +For example, consider you have the following middleware: -```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" +```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports" import { MedusaNextFunction, MedusaRequest, @@ -160,48 +223,124 @@ import { export default defineMiddlewares({ routes: [ { - matcher: "/custom*", - method: ["POST", "PUT"], + matcher: "/custom", middlewares: [ - // ... + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + console.log("Received a request!") + + next() + }, ], }, ], }) ``` -`method`'s value is one or more HTTP methods to apply the middleware to. +If you send a request to `http://localhost:9000/custom`, the middleware will run. + +However, if you send a request to `http://localhost:9000/custom/`, the middleware won't run. -This example applies the middleware only when a `POST` or `PUT` request is sent to an API route path starting with `/custom`. +In general, avoid adding trailing backslashes when sending requests to API routes. --- -## Request URLs with Trailing Backslashes +## Middlewares and Route Ordering -A middleware whose `matcher` pattern doesn't end with a backslash won't be applied for requests to URLs with a trailing backslash. +<Note> -For example, consider you have the following middleware: +The ordering explained in this section was added in [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6) -```ts collapsibleLines="1-7" expandMoreLabel="Show Imports" -import { - MedusaNextFunction, - MedusaRequest, - MedusaResponse, - defineMiddlewares, -} from "@medusajs/framework/http" +</Note> + +The Medusa application registers middlewares and API route handlers in the following order: + +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. + +### Middlewares Sorting + +On top of the previous ordering, Medusa sorts global and route middlewares based on their matcher pattern in the following order: +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`. + +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: [ - ( - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - console.log("Received a request!") + middlewares: [/* ... */], + }, + { + matcher: "/custom*", + method: ["GET"] + middlewares: [/* ... */], + }, + { + matcher: "/custom/:id", + method: ["GET"] + middlewares: [/* ... */], + }, + ], +}) +``` + +The global middlewares are sorted into the following order before they're registered: + +1. Global middleware `/custom`. +2. Global middleware `/custom/:id`. + +And the route middlewares are sorted into the following order before they're registered: + +1. Route middleware `/custom*`. +2. Route middleware `/custom/:id`. +### Middlewares and Route Execution Order + +When a request is sent to an API route, the global middlewares are executed first, then the route middlewares, and finally the route handler. + +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() }, ], @@ -210,16 +349,20 @@ export default defineMiddlewares({ }) ``` -If you send a request to `http://localhost:9000/custom`, the middleware will run. +When you send a request to `/custom` route, the following messages are logged in the terminal: -However, if you send a request to `http://localhost:9000/custom/`, the middleware won't run. +```bash +Global middleware +Route middleware +Hello from custom! # message logged from API route handler +``` -In general, avoid adding trailing backslashes when sending requests to API routes. +The global middleware runs first, then the route middleware, and finally the route handler, assuming that it logs the message `Hello from custom!`. --- -## Middlewares Precedence in Registration +## Overriding Middlewares -The Medusa application registers your middlewares first, then registers middlewares defined in Medusa's core. +A middleware can never override an existing middleware. Instead, middlewares are added to the end of the middleware stack. -So, if you add a middleware for a route defined in the core, it might get overridden by the core middleware. For example, if you add a middleware to change authentication of admin routes, the authentication middleware defined in the core will still run, leading to your middleware not being effective. +For example, if you define a custom validation middleware on an existing route, such as `validateAndTransformBody`, then both the original and the custom validation middleware will run. diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index 07251de07f59a..69b3c48678273 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -53,7 +53,7 @@ export const generatedEditDates = { "app/learn/fundamentals/admin/tips/page.mdx": "2025-02-24T09:52:06.901Z", "app/learn/fundamentals/api-routes/cors/page.mdx": "2024-12-09T13:04:04.357Z", "app/learn/fundamentals/admin/ui-routes/page.mdx": "2025-02-24T09:35:11.752Z", - "app/learn/fundamentals/api-routes/middlewares/page.mdx": "2025-02-12T17:05:20.708Z", + "app/learn/fundamentals/api-routes/middlewares/page.mdx": "2025-03-04T09:46:35.475Z", "app/learn/fundamentals/modules/isolation/page.mdx": "2024-12-09T11:02:38.087Z", "app/learn/fundamentals/data-models/configure-properties/page.mdx": "2024-10-21T13:30:21.368Z", "app/learn/fundamentals/data-models/index/page.mdx": "2024-10-21T13:30:21.368Z", diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index 39b1b40bee34a..92b3931972e1b 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -131,6 +131,73 @@ The next chapter covers how you generally deploy the production build. You can also refer to the [deployment how-to guides](https://docs.medusajs.com/resources/deployment/index.html.md) for platform-specific how-to guides. +# Debugging and Testing + +In the next chapters, you’ll learn about the tools Medusa provides for testing and debugging your Medusa application. + +By the end of this chapter, you’ll learn: + +- How to use Medusa's `@medusajs/test-utils` test to write integration tests. +- How to use Medusa’s `Logger` utility to log messages. + + +# Medusa Deployment Overview + +In this chapter, you’ll learn the general approach to deploying the Medusa application. + +## Medusa Project Components + +A standard Medusa project is made up of: + +- Medusa application: The Medusa server and the Medusa Admin. +- One or more storefronts + + + +You deploy the Medusa application, with the server and admin, separately from the storefront. + +*** + +## Deploying the Medusa Application + +You must deploy the Medusa application before the storefront, as it connects to the server and won’t work without a deployed Medusa server URL. + +The Medusa application must be deployed to a hosting provider supporting Node.js server deployments, such as Railway, DigitalOcean, AWS, Heroku, etc… + + + +Your server connects to a PostgreSQL database, Redis, and other services relevant for your setup. Most hosting providers support deploying and managing these databases along with your Medusa server (such as Railway and DigitalOcean). + +When you deploy your Medusa application, you also deploy the Medusa Admin. For optimal experience, your hosting provider and plan must offer at least 2GB of RAM. + +### How to Deploy Medusa? + +Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance. + +With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: + +- Push to deploy. +- Multiple testing environments. +- Preview environments for new PRs. +- Test on production-like data. + +[Sign up and learn more about Medusa Cloud](https://medusajs.com/contact) + +To self-host Medusa, the [next chapter](https://docs.medusajs.com/learn/deployment/general/index.html.md) explains the general steps to deploy your Medusa application. Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for general and specific hosting providers. + +*** + +## Deploying the Storefront + +The storefront is deployed separately from the Medusa application, and the hosting options depend on the tools and frameworks you use to create the storefront. + +If you’re using the Next.js Starter storefront, you may deploy the storefront to any hosting provider that supports frontend frameworks, such as Vercel. + +Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the storefront on the free plan for development purposes, but for commercial projects, you must update your Vercel plan. + +Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for specific hosting providers. + + # Install Medusa In this chapter, you'll learn how to install and run a Medusa application. @@ -255,63 +322,6 @@ Refer to [this documentation](https://docs.medusajs.com/learn/update/index.html. In the next chapters, you'll learn about the architecture of your Medusa application, then learn how to customize your application to build custom features. -# Medusa Deployment Overview - -In this chapter, you’ll learn the general approach to deploying the Medusa application. - -## Medusa Project Components - -A standard Medusa project is made up of: - -- Medusa application: The Medusa server and the Medusa Admin. -- One or more storefronts - - - -You deploy the Medusa application, with the server and admin, separately from the storefront. - -*** - -## Deploying the Medusa Application - -You must deploy the Medusa application before the storefront, as it connects to the server and won’t work without a deployed Medusa server URL. - -The Medusa application must be deployed to a hosting provider supporting Node.js server deployments, such as Railway, DigitalOcean, AWS, Heroku, etc… - - - -Your server connects to a PostgreSQL database, Redis, and other services relevant for your setup. Most hosting providers support deploying and managing these databases along with your Medusa server (such as Railway and DigitalOcean). - -When you deploy your Medusa application, you also deploy the Medusa Admin. For optimal experience, your hosting provider and plan must offer at least 2GB of RAM. - -### How to Deploy Medusa? - -Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance. - -With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: - -- Push to deploy. -- Multiple testing environments. -- Preview environments for new PRs. -- Test on production-like data. - -[Sign up and learn more about Medusa Cloud](https://medusajs.com/contact) - -To self-host Medusa, the [next chapter](https://docs.medusajs.com/learn/deployment/general/index.html.md) explains the general steps to deploy your Medusa application. Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for general and specific hosting providers. - -*** - -## Deploying the Storefront - -The storefront is deployed separately from the Medusa application, and the hosting options depend on the tools and frameworks you use to create the storefront. - -If you’re using the Next.js Starter storefront, you may deploy the storefront to any hosting provider that supports frontend frameworks, such as Vercel. - -Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the storefront on the free plan for development purposes, but for commercial projects, you must update your Vercel plan. - -Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for specific hosting providers. - - # More Resources The Development Resources documentation provides guides and references that are useful for your development. This documentation included links to parts of the Development Resources documentation where necessary. @@ -341,16 +351,6 @@ Then, when you retrieve products, only products of those sales channels are retr Learn more about passing the publishable API key in [this storefront development guide](https://docs.medusajs.com/resources/storefront-development/publishable-api-keys/index.html.md). -# Debugging and Testing - -In the next chapters, you’ll learn about the tools Medusa provides for testing and debugging your Medusa application. - -By the end of this chapter, you’ll learn: - -- How to use Medusa's `@medusajs/test-utils` test to write integration tests. -- How to use Medusa’s `Logger` utility to log messages. - - # Updating Medusa In this chapter, you'll learn about updating your Medusa application and packages. @@ -501,112 +501,350 @@ import { BrandModuleService } from "@/modules/brand/service" ``` -# Build Custom Features +# Configure Instrumentation -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. +In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. -By following these guides, you'll add brands to the Medusa application that you can associate with products. +## Observability with OpenTelemtry -To build a custom feature in Medusa, you need three main tools: +Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for: -- [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. +- HTTP requests +- Workflow executions +- Query usages +- Database queries and operations - +*** + +## How to Configure Instrumentation in Medusa? + +### Prerequisites + +- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html) + +### Install Dependencies + +Start by installing the following OpenTelemetry dependencies in your Medusa project: + +```bash npm2yarn +npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg +``` + +Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies: + +```bash npm2yarn +npm install @opentelemetry/exporter-zipkin +``` + +### Add instrumentation.ts + +Next, create the file `instrumentation.ts` with the following content: + +```ts title="instrumentation.ts" +import { registerOtel } from "@medusajs/medusa" +import { ZipkinExporter } from "@opentelemetry/exporter-zipkin" + +// If using an exporter other than Zipkin, initialize it here. +const exporter = new ZipkinExporter({ + serviceName: "my-medusa-project", +}) + +export function register() { + registerOtel({ + serviceName: "medusajs", + // pass exporter + exporter, + instrument: { + http: true, + workflows: true, + query: true, + }, + }) +} +``` + +In the `instrumentation.ts` file, you export a `register` function that uses Medusa's `registerOtel` utility function. You also initialize an instance of the exporter, such as Zipkin, and pass it to the `registerOtel` function. + +`registerOtel` accepts an object where you can pass any [NodeSDKConfiguration](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_node.NodeSDKConfiguration.html) property along with the following properties: + +The `NodeSDKConfiguration` properties are accepted since Medusa v2.5.1. + +- serviceName: (\`string\`) The name of the service traced. +- exporter: (\[SpanExporter]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_sdk\_trace\_base.SpanExporter.html)) An instance of an exporter, such as Zipkin. +- instrument: (\`object\`) Options specifying what to trace. + + - http: (\`boolean\`) Whether to trace HTTP requests. + + - query: (\`boolean\`) Whether to trace Query usages. + + - workflows: (\`boolean\`) Whether to trace Workflow executions. + + - db: (\`boolean\`) Whether to trace database queries and operations. +- instrumentations: (\[Instrumentation\[]]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_instrumentation.Instrumentation.html)) Additional instrumentation options that OpenTelemetry accepts. *** -## Next Chapters: Brand Module Example +## Test it Out -The next chapters will guide you to: +To test it out, start your exporter, such as Zipkin. -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. +Then, start your Medusa application: +```bash npm2yarn +npm run dev +``` -# Customize Medusa Admin Dashboard +Try to open the Medusa Admin or send a request to an API route. -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). +If you check traces in your exporter, you'll find new traces reported. -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: +### Trace Span Names -- 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). +Trace span names start with the following keywords based on what it's reporting: -From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard +- `{methodName} {URL}` when reporting HTTP requests, where `{methodName}` is the HTTP method, and `{URL}` is the URL the request is sent to. +- `route:` when reporting route handlers running on an HTTP request. +- `middleware:` when reporting a middleware running on an HTTP request. +- `workflow:` when reporting a workflow execution. +- `step:` when reporting a step in a workflow execution. +- `query.graph:` when reporting Query usages. +- `pg.query:` when reporting database queries and operations. + + +# Logging + +In this chapter, you’ll learn how to use Medusa’s logging utility. + +## Logger Class + +Medusa provides a `Logger` class with advanced logging functionalities. This includes configuring logging levels or saving logs to a file. + +The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`. *** -## Next Chapters: View Brands in Dashboard +## How to Log a Message -In the next chapters, you'll continue with the brands example to: +Resolve the `logger` using the Medusa container to log a message in your resource. -- 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. +For example, create the file `src/jobs/log-message.ts` with the following content: +```ts title="src/jobs/log-message.ts" highlights={highlights} +import { MedusaContainer } from "@medusajs/framework/types" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -# Extend Core Commerce Features +export default async function myCustomJob( + container: MedusaContainer +) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) -In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features. + logger.info("I'm using the logger!") +} -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. +export const config = { + name: "test-logger", + // execute every minute + schedule: "* * * * *", +} +``` -Medusa's framework and orchestration tools mitigate these issues while supporting all your customization needs: +This creates a scheduled job that resolves the `logger` from the Medusa container and uses it to log a message. -- [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. +### Test the Scheduled Job + +To test out the above scheduled job, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +After a minute, you'll see the following message as part of the logged messages: + +```text +info: I'm using the logger! +``` *** -## Next Chapters: Link Brands to Products Example +## Log Levels -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: +The `Logger` class has the following methods: -- 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. +- `info`: The message is logged with level `info`. +- `warn`: The message is logged with level `warn`. +- `error`: The message is logged with level `error`. +- `debug`: The message is logged with level `debug`. +Each of these methods accepts a string parameter to log in the terminal with the associated level. -# 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. +## Logging Configurations -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. +### Log Level -## Helpful Resources Guides +The available log levels, from lowest to highest levels, are: -The [Development Resources](https://docs.medusajs.com/resources/index.html.md) documentation provides more helpful guides and references for your development journey. Some of these guides and references include: +1. `silly` (default, meaning messages of all levels are logged) +2. `debug` +3. `info` +4. `warn` +5. `error` -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. +You can change that by setting the `LOG_LEVEL` environment variable to the minimum level you want to be logged. + +For example: + +```bash +LOG_LEVEL=error +``` + +This logs `error` messages only. + +The environment variable must be set as a system environment variable and not in `.env`. + +### Save Logs in a File + +Aside from showing the logs in the terminal, you can save the logs in a file by setting the `LOG_FILE` environment variable to the path of the file relative to the Medusa server’s root directory. + +For example: + +```bash +LOG_FILE=all.log +``` + +Your logs are now saved in the `all.log` file at the root of your Medusa application. + +The environment variable must be set as a system environment variable and not in `.env`. *** -## More Examples in Recipes +## Show Log with Progress -In the Development Resources documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more. +The `Logger` class has an `activity` method used to log a message of level `info`. If the Medusa application is running in a development environment, a spinner starts to show the activity's progress. -Refer to the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation to learn more. +For example: +```ts title="src/jobs/log-message.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -# Re-Use Customizations with Plugins +export default async function myCustomJob( + container: MedusaContainer +) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) -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. + const activityId = logger.activity("First log message") -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. + logger.progress(activityId, `Second log message`) -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. + logger.success(activityId, "Last log message") +} +``` - +The `activity` method returns the ID of the started activity. This ID can then be passed to one of the following methods of the `Logger` class: -Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM. +- `progress`: Log a message of level `info` that indicates progress within that same activity. +- `success`: Log a message of level `info` that indicates that the activity has succeeded. This also ends the associated activity. +- `failure`: Log a message of level `error` that indicates that the activity has failed. This also ends the associated activity. -To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). +If you configured the `LOG_LEVEL` environment variable to a level higher than those associated with the above methods, their messages won’t be logged. + + +# Medusa Testing Tools + +In this chapter, you'll learn about Medusa's testing tools and how to install and configure them. + +## @medusajs/test-utils Package + +Medusa provides a Testing Framework to create integration tests for your custom API routes, modules, or other Medusa customizations. + +To use the Testing Framework, install `@medusajs/test-utils` as a `devDependency`: + +```bash npm2yarn +npm install --save-dev @medusajs/test-utils@latest +``` + +*** + +## Install and Configure Jest + +Writing tests with `@medusajs/test-utils`'s tools requires installing and configuring Jest in your project. + +Run the following command to install the required Jest dependencies: + +```bash npm2yarn +npm install --save-dev jest @types/jest @swc/jest +``` + +Then, create the file `jest.config.js` with the following content: + +```js title="jest.config.js" +const { loadEnv } = require("@medusajs/framework/utils") +loadEnv("test", process.cwd()) + +module.exports = { + transform: { + "^.+\\.[jt]s$": [ + "@swc/jest", + { + jsc: { + parser: { syntax: "typescript", decorators: true }, + }, + }, + ], + }, + testEnvironment: "node", + moduleFileExtensions: ["js", "ts", "json"], + modulePathIgnorePatterns: ["dist/"], + setupFiles: ["./integration-tests/setup.js"], +} + +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"] +} +``` + +Next, create the `integration-tests/setup.js` file with the following content: + +```js title="integration-tests/setup.js" +const { MetadataStorage } = require("@mikro-orm/core") + +MetadataStorage.clear() +``` + +*** + +## Add Test Commands + +Finally, add the following scripts to `package.json`: + +```json title="package.json" +"scripts": { + // ... + "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit", + "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit", + "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" +}, +``` + +You now have two commands: + +- `test:integration:http` to run integration tests (for example, for API routes and workflows) available under the `integration-tests/http` directory. +- `test:integration:modules` to run integration tests for modules available in any `__tests__` directory under `src/modules`. +- `test:unit` to run unit tests in any `__tests__` directory under the `src` directory. + +Medusa's Testing Framework works for integration tests only. You can write unit tests using Jest. + +*** + +## Test Tools and Writing Tests + +The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests. # General Medusa Application Deployment Guide @@ -910,34 +1148,266 @@ 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. -# Integrate Third-Party Systems +# Build Custom Features -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. +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. -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. +By following these guides, you'll add brands to the Medusa application that you can associate with products. -In Medusa, you integrate a third-party system by: +To build a custom feature in Medusa, you need three main tools: -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. +- [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. + + *** -## Next Chapters: Sync Brands Example +## Next Chapters: Brand Module 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: +The next chapters will guide you to: -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. +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. -# Admin Development +# Customize Medusa Admin Dashboard -In the next chapters, you'll learn more about possible admin customizations. +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). -You can customize the admin dashboard by: +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. + + +# 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. + + + +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). + + +# 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. + +## Helpful Resources Guides + +The [Development Resources](https://docs.medusajs.com/resources/index.html.md) documentation provides more helpful guides and references for your development journey. Some of these guides and references include: + +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 Development Resources documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more. + +Refer to the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation to learn more. + + +# 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's Architecture + +In this chapter, you'll learn about the architectural layers in Medusa. + +## HTTP, Workflow, and Module Layers + +Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes. + +In a common Medusa application, requests go through four layers in the stack. In order of entry, those are: + +1. API Routes (HTTP): Our API Routes are the typical entry point. +2. Workflows: API Routes consume workflows that hold the opinionated business logic of your application. +3. Modules: Workflows use domain-specific modules for resource management. +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). + + + +*** + +## Database Layer + +The Medusa application injects into each module a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. + +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + + + +*** + +## Service Integrations + +Third-party services are integrated through commerce and architectural modules. You also create custom third-party integrations through a custom module. + +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + +### Commerce Modules + +[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 integrate Stripe through a payment module provider. + + + +### Architectural Modules + +[Architectural modules](https://docs.medusajs.com/resources/architectural-modules/index.html.md) integrate third-party services and systems for architectural features. For example, you integrate Redis as a pub/sub service to send events, or SendGrid to send notifications. + + + +*** + +## Full Diagram of Medusa's Architecture + +The following diagram illustrates Medusa's architecture over the three layers. + + + + +# Next.js Starter Storefront + +The Medusa application is made up of a Node.js server and an admin dashboard. The storefront is installed and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience. + +The Next.js Starter storefront provides rich commerce features and a sleek design. Developers and businesses can use it as-is or build on top of it to tailor it for the business's unique use case, design, and customer experience. + +In this chapter, you’ll learn how to install the Next.js Starter storefront separately from the Medusa application. You can also install it while installing the Medusa application as explained in [the installation chapter](https://docs.medusajs.com/learn/installation/index.html.md). + +## Install Next.js Starter + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) + +If you already have a Medusa application installed with at least one region, you can install the Next.js Starter storefront with the following steps: + +1. Clone the [Next.js Starter](https://github.com/medusajs/nextjs-starter-medusa): + +```bash +git clone https://github.com/medusajs/nextjs-starter-medusa my-medusa-storefront +``` + +2. Change to the `my-medusa-storefront` directory, install the dependencies, and rename the template environment variable file: + +```bash npm2yarn +cd my-medusa-storefront +npm install +mv .env.template .env.local +``` + +3. Set the Medusa application's publishable API key in the `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY` environment variable. You can retrieve the publishable API key in on the Medusa Admin dashboard by going to Settings -> Publishable API Keys + +```bash +NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_123... +``` + +4. While the Medusa application is running, start the Next.js Starter storefront: + +```bash npm2yarn +npm run dev +``` + +Your Next.js Starter storefront is now running at `http://localhost:8000`. + +*** + +## Customize Storefront + +To customize the storefront, refer to the following directories: + +- `src/app`: The storefront’s pages. +- `src/modules`: The storefront’s components. +- `src/styles`: The storefront’s styles. + +You can learn more about development with Next.js through [their documentation](https://nextjs.org/docs/getting-started). + +*** + +## Configurations and Integrations + +The Next.js Starter is compatible with some Medusa integrations out-of-the-box, such as the Stripe provider module. You can also change some of its configurations if necessary. + +Refer to the [Next.js Starter reference](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) for more details. + + +# 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. @@ -1417,19 +1887,6 @@ Medusa provides two Event Modules out of the box: 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. - - # Module Link In this chapter, you’ll learn what a module link is. @@ -1598,6 +2055,19 @@ The next chapter explains how you can create and publish a plugin. For more resources and guides related to plugins, refer to the [Resources documentation](https://docs.medusajs.com/resources/plugins/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. + + # Modules In this chapter, you’ll learn about modules and how to create them. @@ -1898,64 +2368,6 @@ 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 - -In this chapter, you'll learn about the architectural layers in Medusa. - -## HTTP, Workflow, and Module Layers - -Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes. - -In a common Medusa application, requests go through four layers in the stack. In order of entry, those are: - -1. API Routes (HTTP): Our API Routes are the typical entry point. -2. Workflows: API Routes consume workflows that hold the opinionated business logic of your application. -3. Modules: Workflows use domain-specific modules for resource management. -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). - - - -*** - -## Database Layer - -The Medusa application injects into each module a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. - -Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - - - -*** - -## Service Integrations - -Third-party services are integrated through commerce and architectural modules. You also create custom third-party integrations through a custom module. - -Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - -### Commerce Modules - -[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 integrate Stripe through a payment module provider. - - - -### Architectural Modules - -[Architectural modules](https://docs.medusajs.com/resources/architectural-modules/index.html.md) integrate third-party services and systems for architectural features. For example, you integrate Redis as a pub/sub service to send events, or SendGrid to send notifications. - - - -*** - -## Full Diagram of Medusa's Architecture - -The following diagram illustrates Medusa's architecture over the three layers. - - - - # Scheduled Jobs In this chapter, you’ll learn about scheduled jobs and how to use them. @@ -2050,72 +2462,6 @@ In the scheduled job function, you execute the `syncProductToErpWorkflow` by inv The next time you start the Medusa application, it will run this job every day at midnight. -# Next.js Starter Storefront - -The Medusa application is made up of a Node.js server and an admin dashboard. The storefront is installed and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience. - -The Next.js Starter storefront provides rich commerce features and a sleek design. Developers and businesses can use it as-is or build on top of it to tailor it for the business's unique use case, design, and customer experience. - -In this chapter, you’ll learn how to install the Next.js Starter storefront separately from the Medusa application. You can also install it while installing the Medusa application as explained in [the installation chapter](https://docs.medusajs.com/learn/installation/index.html.md). - -## Install Next.js Starter - -### Prerequisites - -- [Node.js v20+](https://nodejs.org/en/download) -- [Git CLI tool](https://git-scm.com/downloads) - -If you already have a Medusa application installed with at least one region, you can install the Next.js Starter storefront with the following steps: - -1. Clone the [Next.js Starter](https://github.com/medusajs/nextjs-starter-medusa): - -```bash -git clone https://github.com/medusajs/nextjs-starter-medusa my-medusa-storefront -``` - -2. Change to the `my-medusa-storefront` directory, install the dependencies, and rename the template environment variable file: - -```bash npm2yarn -cd my-medusa-storefront -npm install -mv .env.template .env.local -``` - -3. Set the Medusa application's publishable API key in the `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY` environment variable. You can retrieve the publishable API key in on the Medusa Admin dashboard by going to Settings -> Publishable API Keys - -```bash -NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_123... -``` - -4. While the Medusa application is running, start the Next.js Starter storefront: - -```bash npm2yarn -npm run dev -``` - -Your Next.js Starter storefront is now running at `http://localhost:8000`. - -*** - -## Customize Storefront - -To customize the storefront, refer to the following directories: - -- `src/app`: The storefront’s pages. -- `src/modules`: The storefront’s components. -- `src/styles`: The storefront’s styles. - -You can learn more about development with Next.js through [their documentation](https://nextjs.org/docs/getting-started). - -*** - -## Configurations and Integrations - -The Next.js Starter is compatible with some Medusa integrations out-of-the-box, such as the Stripe provider module. You can also change some of its configurations if necessary. - -Refer to the [Next.js Starter reference](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) for more details. - - # Workflows In this chapter, you’ll learn about workflows and how to define and execute them. @@ -2370,212 +2716,6 @@ 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. -# Configure Instrumentation - -In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. - -## Observability with OpenTelemtry - -Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for: - -- HTTP requests -- Workflow executions -- Query usages -- Database queries and operations - -*** - -## How to Configure Instrumentation in Medusa? - -### Prerequisites - -- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html) - -### Install Dependencies - -Start by installing the following OpenTelemetry dependencies in your Medusa project: - -```bash npm2yarn -npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg -``` - -Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies: - -```bash npm2yarn -npm install @opentelemetry/exporter-zipkin -``` - -### Add instrumentation.ts - -Next, create the file `instrumentation.ts` with the following content: - -```ts title="instrumentation.ts" -import { registerOtel } from "@medusajs/medusa" -import { ZipkinExporter } from "@opentelemetry/exporter-zipkin" - -// If using an exporter other than Zipkin, initialize it here. -const exporter = new ZipkinExporter({ - serviceName: "my-medusa-project", -}) - -export function register() { - registerOtel({ - serviceName: "medusajs", - // pass exporter - exporter, - instrument: { - http: true, - workflows: true, - query: true, - }, - }) -} -``` - -In the `instrumentation.ts` file, you export a `register` function that uses Medusa's `registerOtel` utility function. You also initialize an instance of the exporter, such as Zipkin, and pass it to the `registerOtel` function. - -`registerOtel` accepts an object where you can pass any [NodeSDKConfiguration](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_node.NodeSDKConfiguration.html) property along with the following properties: - -The `NodeSDKConfiguration` properties are accepted since Medusa v2.5.1. - -- serviceName: (\`string\`) The name of the service traced. -- exporter: (\[SpanExporter]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_sdk\_trace\_base.SpanExporter.html)) An instance of an exporter, such as Zipkin. -- instrument: (\`object\`) Options specifying what to trace. - - - http: (\`boolean\`) Whether to trace HTTP requests. - - - query: (\`boolean\`) Whether to trace Query usages. - - - workflows: (\`boolean\`) Whether to trace Workflow executions. - - - db: (\`boolean\`) Whether to trace database queries and operations. -- instrumentations: (\[Instrumentation\[]]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_instrumentation.Instrumentation.html)) Additional instrumentation options that OpenTelemetry accepts. - -*** - -## Test it Out - -To test it out, start your exporter, such as Zipkin. - -Then, start your Medusa application: - -```bash npm2yarn -npm run dev -``` - -Try to open the Medusa Admin or send a request to an API route. - -If you check traces in your exporter, you'll find new traces reported. - -### Trace Span Names - -Trace span names start with the following keywords based on what it's reporting: - -- `{methodName} {URL}` when reporting HTTP requests, where `{methodName}` is the HTTP method, and `{URL}` is the URL the request is sent to. -- `route:` when reporting route handlers running on an HTTP request. -- `middleware:` when reporting a middleware running on an HTTP request. -- `workflow:` when reporting a workflow execution. -- `step:` when reporting a step in a workflow execution. -- `query.graph:` when reporting Query usages. -- `pg.query:` when reporting database queries and operations. - - -# Medusa Testing Tools - -In this chapter, you'll learn about Medusa's testing tools and how to install and configure them. - -## @medusajs/test-utils Package - -Medusa provides a Testing Framework to create integration tests for your custom API routes, modules, or other Medusa customizations. - -To use the Testing Framework, install `@medusajs/test-utils` as a `devDependency`: - -```bash npm2yarn -npm install --save-dev @medusajs/test-utils@latest -``` - -*** - -## Install and Configure Jest - -Writing tests with `@medusajs/test-utils`'s tools requires installing and configuring Jest in your project. - -Run the following command to install the required Jest dependencies: - -```bash npm2yarn -npm install --save-dev jest @types/jest @swc/jest -``` - -Then, create the file `jest.config.js` with the following content: - -```js title="jest.config.js" -const { loadEnv } = require("@medusajs/framework/utils") -loadEnv("test", process.cwd()) - -module.exports = { - transform: { - "^.+\\.[jt]s$": [ - "@swc/jest", - { - jsc: { - parser: { syntax: "typescript", decorators: true }, - }, - }, - ], - }, - testEnvironment: "node", - moduleFileExtensions: ["js", "ts", "json"], - modulePathIgnorePatterns: ["dist/"], - setupFiles: ["./integration-tests/setup.js"], -} - -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"] -} -``` - -Next, create the `integration-tests/setup.js` file with the following content: - -```js title="integration-tests/setup.js" -const { MetadataStorage } = require("@mikro-orm/core") - -MetadataStorage.clear() -``` - -*** - -## Add Test Commands - -Finally, add the following scripts to `package.json`: - -```json title="package.json" -"scripts": { - // ... - "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit", - "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit", - "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" -}, -``` - -You now have two commands: - -- `test:integration:http` to run integration tests (for example, for API routes and workflows) available under the `integration-tests/http` directory. -- `test:integration:modules` to run integration tests for modules available in any `__tests__` directory under `src/modules`. -- `test:unit` to run unit tests in any `__tests__` directory under the `src` directory. - -Medusa's Testing Framework works for integration tests only. You can write unit tests using Jest. - -*** - -## Test Tools and Writing Tests - -The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests. - - # Guide: Create Brand API Route 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. @@ -2940,282 +3080,205 @@ The Brand Module now creates a `brand` table in the database and provides a clas 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. -# Guide: Create Brand Workflow - -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. - -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. +# Write Tests for Modules -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). +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 -- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) - -*** - -## 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 - -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: +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) - +## moduleIntegrationTestRunner Utility -```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" +`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. -export type CreateBrandStepInput = { - name: string -} +For example, assuming you have a `hello` module, create a test file at `src/modules/hello/__tests__/service.spec.ts`: -export const createBrandStep = createStep( - "create-brand-step", - async (input: CreateBrandStepInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +```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" - const brand = await brandModuleService.createBrands(input) +moduleIntegrationTestRunner<HelloModuleService>({ + moduleName: HELLO_MODULE, + moduleModels: [MyCustom], + resolve: "./src/modules/hello", + testSuite: ({ service }) => { + // TODO write tests + }, +}) - return new StepResponse(brand, brand.id) - } -) +jest.setTimeout(60 * 1000) ``` -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. - -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. - -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. +The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties: -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). +- `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. -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. +The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service. -### Add Compensation Function to Step +The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property. -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. +The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). -Learn more about the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). +*** -To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`: +## Run Tests -```ts title="src/workflows/create-brand.ts" -export const createBrandStep = createStep( - // ... - async (id: string, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +Run the following command to run your module integration tests: - await brandModuleService.deleteBrands(id) - } -) +```bash npm2yarn +npm run test:integration:modules ``` -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. - -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. - -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). +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). -So, if an error occurs during the workflow's execution, the brand that was created by the step is deleted to maintain data consistency. +This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. *** -## 2. Create createBrandWorkflow - -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: - -```ts title="src/workflows/create-brand.ts" -// other imports... -import { - // ... - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +## Pass Module Options -// ... +If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter. -type CreateBrandWorkflowInput = { - name: string -} +For example: -export const createBrandWorkflow = createWorkflow( - "create-brand", - (input: CreateBrandWorkflowInput) => { - const brand = createBrandStep(input) +```ts +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import HelloModuleService from "../service" - return new WorkflowResponse(brand) - } -) +moduleIntegrationTestRunner<HelloModuleService>({ + moduleOptions: { + apiKey: "123", + }, + // ... +}) ``` -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. - -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. - -A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor. - *** -## Next Steps: Expose Create Brand API Route - -You now have a `createBrandWorkflow` that you can execute to create a brand. - -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. +## Write Tests for Modules without Data Models +If your module doesn't have a data model, pass a dummy model in the `moduleModels` property. -# Logging +For example: -In this chapter, you’ll learn how to use Medusa’s logging utility. +```ts +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import HelloModuleService from "../service" +import { model } from "@medusajs/framework/utils" -## Logger Class +const DummyModel = model.define("dummy_model", { + id: model.id().primaryKey(), +}) -Medusa provides a `Logger` class with advanced logging functionalities. This includes configuring logging levels or saving logs to a file. +moduleIntegrationTestRunner<HelloModuleService>({ + moduleModels: [DummyModel], + // ... +}) -The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`. +jest.setTimeout(60 * 1000) +``` *** -## How to Log a Message - -Resolve the `logger` using the Medusa container to log a message in your resource. - -For example, create the file `src/jobs/log-message.ts` with the following content: +### Other Options and Inputs -```ts title="src/jobs/log-message.ts" highlights={highlights} -import { MedusaContainer } from "@medusajs/framework/types" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +Refer to [this reference in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. -export default async function myCustomJob( - container: MedusaContainer -) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) +*** - logger.info("I'm using the logger!") -} +## Database Used in Tests -export const config = { - name: "test-logger", - // execute every minute - schedule: "* * * * *", -} -``` +The `moduleIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. -This creates a scheduled job that resolves the `logger` from the Medusa container and uses it to log a message. +To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). -### Test the Scheduled Job -To test out the above scheduled job, start the Medusa application: +# Write Integration Tests -```bash npm2yarn -npm run dev -``` +In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests. -After a minute, you'll see the following message as part of the logged messages: +### Prerequisites -```text -info: I'm using the logger! -``` +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -*** +## medusaIntegrationTestRunner Utility -## Log Levels +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. -The `Logger` class has the following methods: +For example: -- `info`: The message is logged with level `info`. -- `warn`: The message is logged with level `warn`. -- `error`: The message is logged with level `error`. -- `debug`: The message is logged with level `debug`. +```ts title="integration-tests/http/test.spec.ts" highlights={highlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -Each of these methods accepts a string parameter to log in the terminal with the associated level. +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + // TODO write tests... + }, +}) -*** +jest.setTimeout(60 * 1000) +``` -## Logging Configurations +The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`. -### Log Level +`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: -The available log levels, from lowest to highest levels, are: +- `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. -1. `silly` (default, meaning messages of all levels are logged) -2. `debug` -3. `info` -4. `warn` -5. `error` +The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). -You can change that by setting the `LOG_LEVEL` environment variable to the minimum level you want to be logged. +### 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: -```bash -LOG_LEVEL=error +```ts title="integration-tests/http/test.spec.ts" +// in your test's file +jest.setTimeout(60 * 1000) ``` -This logs `error` messages only. - -The environment variable must be set as a system environment variable and not in `.env`. - -### Save Logs in a File +*** -Aside from showing the logs in the terminal, you can save the logs in a file by setting the `LOG_FILE` environment variable to the path of the file relative to the Medusa server’s root directory. +### Run Tests -For example: +Run the following command to run your tests: -```bash -LOG_FILE=all.log +```bash npm2yarn +npm run test:integration ``` -Your logs are now saved in the `all.log` file at the root of your Medusa application. +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). -The environment variable must be set as a system environment variable and not in `.env`. +This runs your Medusa application and runs the tests available under the `src/integrations/http` directory. *** -## Show Log with Progress - -The `Logger` class has an `activity` method used to log a message of level `info`. If the Medusa application is running in a development environment, a spinner starts to show the activity's progress. - -For example: +## Other Options and Inputs -```ts title="src/jobs/log-message.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +Refer to [this reference in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. -export default async function myCustomJob( - container: MedusaContainer -) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) +*** - const activityId = logger.activity("First log message") +## Database Used in Tests - logger.progress(activityId, `Second log message`) +The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. - logger.success(activityId, "Last log message") -} -``` +To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md). -The `activity` method returns the ID of the started activity. This ID can then be passed to one of the following methods of the `Logger` class: +*** -- `progress`: Log a message of level `info` that indicates progress within that same activity. -- `success`: Log a message of level `info` that indicates that the activity has succeeded. This also ends the associated activity. -- `failure`: Log a message of level `error` that indicates that the activity has failed. This also ends the associated activity. +## Example Integration Tests -If you configured the `LOG_LEVEL` environment variable to a level higher than those associated with the above methods, their messages won’t be logged. +The next chapters provide examples of writing integration tests for API routes and workflows. # Create Brands UI Route in Admin @@ -3744,6 +3807,144 @@ The [Admin Components guides](https://docs.medusajs.com/resources/admin-componen In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users. +# Guide: Create Brand Workflow + +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. + +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. + +Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). + +### Prerequisites + +- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) + +*** + +## 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 + +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: + + + +```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" + +export type CreateBrandStepInput = { + name: string +} + +export const createBrandStep = createStep( + "create-brand-step", + async (input: CreateBrandStepInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + const brand = await brandModuleService.createBrands(input) + + return new StepResponse(brand, brand.id) + } +) +``` + +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. + +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. + +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. + +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). + +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. + +### Add Compensation Function to Step + +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). + +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 + ) + + await brandModuleService.deleteBrands(id) + } +) +``` + +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. + +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. + +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. + +*** + +## 2. Create createBrandWorkflow + +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: + +```ts title="src/workflows/create-brand.ts" +// other imports... +import { + // ... + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +// ... + +type CreateBrandWorkflowInput = { + name: string +} + +export const createBrandWorkflow = createWorkflow( + "create-brand", + (input: CreateBrandWorkflowInput) => { + const brand = createBrandStep(input) + + return new WorkflowResponse(brand) + } +) +``` + +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. + +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. + +A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor. + +*** + +## Next Steps: Expose Create Brand API Route + +You now have a `createBrandWorkflow` that you can execute to create a brand. + +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. + + # Guide: Define Module Link Between Brand and Product 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. @@ -3814,6 +4015,278 @@ You can also run the `npx medusa db:sync-links` to just sync module links withou 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. +# 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. + +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. + +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. + +Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). + +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. + +### Prerequisites + +- [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 + +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. + +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" + +// ... + +export const createBrandWorkflow = createWorkflow( + "create-brand", + (input: CreateBrandInput) => { + // ... + + emitEventStep({ + eventName: "brand.created", + data: { + id: brand.id, + }, + }) + + return new WorkflowResponse(brand) + } +) +``` + +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. + +You'll learn how to handle this event in a later step. + +*** + +## 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 + +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: + + + +```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" + +type SyncBrandToCmsStepInput = { + brand: InferTypeOf<typeof Brand> +} + +const syncBrandToCmsStep = createStep( + "sync-brand-to-cms", + async ({ brand }: SyncBrandToCmsStepInput, { container }) => { + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) + + await cmsModuleService.createBrand(brand) + + return new StepResponse(null, brand.id) + }, + async (id, { container }) => { + if (!id) { + return + } + + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) + + await cmsModuleService.deleteBrand(id) + } +) +``` + +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. + +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: + +```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" + +// ... + +type SyncBrandToCmsWorkflowInput = { + id: string +} + +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, + }, + }) + + syncBrandToCmsStep({ + brand: brands[0], + } as SyncBrandToCmsStepInput) + + return new WorkflowResponse({}) + } +) +``` + +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. + +You'll execute this workflow in the subscriber next. + +Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). + +*** + +## 3. Handle brand.created Event + +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. + +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/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 config: SubscriberConfig = { + event: "brand.created", +} +``` + +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. + +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. + +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. + +Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). + +*** + +## Test it Out + +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. + +First, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +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" +}' +``` + +Make sure to replace the email and password with your admin user's credentials. + +Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). + +Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: + +```bash +curl -X POST 'http://localhost:9000/admin/brands' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "name": "Acme" +}' +``` + +This 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" +``` + +*** + +## Next Chapter: Sync Brand from Third-Party CMS to Medusa + +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. + + # Guide: Extend Create Product Flow 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. @@ -4471,6 +4944,51 @@ By following the previous chapters, you utilized Medusa's framework and orchestr 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. +# Admin Development Constraints + +This chapter lists some constraints of admin widgets and UI routes. + +## Arrow Functions + +Widget and UI route components must be created as arrow functions. + +```ts highlights={arrowHighlights} +// Don't +function ProductWidget() { + // ... +} + +// Do +const ProductWidget = () => { + // ... +} +``` + +*** + +## Widget Zone + +A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. + +```ts highlights={zoneHighlights} +// Don't +export const config = defineWidgetConfig({ + zone: `product.details.before`, +}) + +// Don't +const ZONE = "product.details.after" +export const config = defineWidgetConfig({ + zone: ZONE, +}) + +// Do +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) +``` + + # Guide: Integrate CMS Brand System 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. @@ -4630,321 +5148,73 @@ You can now use the CMS Module's service to perform actions on the third-party C 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. -# 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. +# Environment Variables in Admin Customizations -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. +In this chapter, you'll learn how to use environment variables in your admin customizations. -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. +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). -Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). +## How to Set Environment Variables -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. +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_`. -### Prerequisites +For example: -- [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) +```plain +VITE_MY_API_KEY=sk_123 +``` -## 1. Emit Event in createBrandWorkflow +*** -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. +## How to Use Environment Variables -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`: +To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object. -```ts title="src/workflows/create-brand.ts" highlights={eventHighlights} -// other imports... -import { - emitEventStep, -} from "@medusajs/medusa/core-flows" +For example: -// ... +```tsx highlights={[["8"]]} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" -export const createBrandWorkflow = createWorkflow( - "create-brand", - (input: CreateBrandInput) => { - // ... +const ProductWidget = () => { + return ( + <Container className="divide-y p-0"> + <div className="flex items-center justify-between px-6 py-4"> + <Heading level="h2">API Key: {import.meta.env.VITE_MY_API_KEY}</Heading> + </div> + </Container> + ) +} - emitEventStep({ - eventName: "brand.created", - data: { - id: brand.id, - }, - }) +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) - return new WorkflowResponse(brand) - } -) -``` - -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. - -You'll learn how to handle this event in a later step. - -*** - -## 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 - -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: - - - -```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" - -type SyncBrandToCmsStepInput = { - brand: InferTypeOf<typeof Brand> -} - -const syncBrandToCmsStep = createStep( - "sync-brand-to-cms", - async ({ brand }: SyncBrandToCmsStepInput, { container }) => { - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) - - await cmsModuleService.createBrand(brand) - - return new StepResponse(null, brand.id) - }, - async (id, { container }) => { - if (!id) { - return - } - - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) - - await cmsModuleService.deleteBrand(id) - } -) -``` - -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. - -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: - -```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" - -// ... - -type SyncBrandToCmsWorkflowInput = { - id: string -} - -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, - }, - }) - - syncBrandToCmsStep({ - brand: brands[0], - } as SyncBrandToCmsStepInput) - - return new WorkflowResponse({}) - } -) -``` - -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. - -You'll execute this workflow in the subscriber next. - -Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). - -*** - -## 3. Handle brand.created Event - -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. - -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/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 config: SubscriberConfig = { - event: "brand.created", -} -``` - -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. - -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. - -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. - -Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). - -*** - -## Test it Out - -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. - -First, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -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" -}' +export default ProductWidget ``` -Make sure to replace the email and password with your admin user's credentials. - -Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). - -Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: +In this example, you display the API key in a widget using `import.meta.env.VITE_MY_API_KEY`. -```bash -curl -X POST 'http://localhost:9000/admin/brands' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data '{ - "name": "Acme" -}' -``` +### Type Error on import.meta.env -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: +If you receive a type error on `import.meta.env`, create the file `src/admin/vite-env.d.ts` with the following content: -```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" +```ts title="src/admin/vite-env.d.ts" +/// <reference types="vite/client" /> ``` -*** - -## Next Chapter: Sync Brand from Third-Party CMS to Medusa - -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. - - -# Admin Development Constraints - -This chapter lists some constraints of admin widgets and UI routes. - -## Arrow Functions - -Widget and UI route components must be created as arrow functions. - -```ts highlights={arrowHighlights} -// Don't -function ProductWidget() { - // ... -} - -// Do -const ProductWidget = () => { - // ... -} -``` +This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables. *** -## 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. +## Check Node Environment in Admin Customizations -```ts highlights={zoneHighlights} -// Don't -export const config = defineWidgetConfig({ - zone: `product.details.before`, -}) +To check the current environment, Vite exposes two variables: -// Don't -const ZONE = "product.details.after" -export const config = defineWidgetConfig({ - zone: ZONE, -}) +- `import.meta.env.DEV`: Returns `true` if the current environment is development. +- `import.meta.env.PROD`: Returns `true` if the current environment is production. -// Do -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) -``` +Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode). # Admin Routing Customizations @@ -5231,44 +5501,46 @@ The Medusa Admin dashboard can be displayed in languages other than English, whi Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/resources/contribution-guidelines/admin-translations/index.html.md). -# Environment Variables in Admin Customizations +# Admin Widgets -In this chapter, you'll learn how to use environment variables in your admin customizations. +In this chapter, you’ll learn more about widgets and how to use them. -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). +## What is an Admin Widget? -## How to Set Environment Variables +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. -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_`. +For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. -For example: +*** -```plain -VITE_MY_API_KEY=sk_123 -``` +## How to Create a Widget? -*** +### Prerequisites -## How to Use Environment Variables +- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) -To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object. +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: +For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: -```tsx highlights={[["8"]]} + + +```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} import { defineWidgetConfig } from "@medusajs/admin-sdk" import { Container, Heading } from "@medusajs/ui" +// The widget const ProductWidget = () => { return ( <Container className="divide-y p-0"> <div className="flex items-center justify-between px-6 py-4"> - <Heading level="h2">API Key: {import.meta.env.VITE_MY_API_KEY}</Heading> + <Heading level="h2">Product Widget</Heading> </div> </Container> ) } +// The widget's configurations export const config = defineWidgetConfig({ zone: "product.details.before", }) @@ -5276,28 +5548,76 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` -In this example, you display the API key in a widget using `import.meta.env.VITE_MY_API_KEY`. +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. -### Type Error on import.meta.env +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. -If you receive a type error on `import.meta.env`, create the file `src/admin/vite-env.d.ts` with the following content: +In the example above, the widget is injected at the top of a product’s details. -```ts title="src/admin/vite-env.d.ts" -/// <reference types="vite/client" /> +The widget component must be created as an arrow function. + +### Test the Widget + +To test out the widget, start the Medusa application: + +```bash npm2yarn +npm run dev ``` -This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables. +Then, open a product’s details page. You’ll find your custom widget at the top of the page. *** -## Check Node Environment in Admin Customizations +## Props Passed in Detail Pages -To check the current environment, Vite exposes two variables: +Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. -- `import.meta.env.DEV`: Returns `true` if the current environment is development. -- `import.meta.env.PROD`: Returns `true` if the current environment is production. +For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: -Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode). +```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" +import { + DetailWidgetProps, + AdminProduct, +} from "@medusajs/framework/types" + +// The widget +const ProductWidget = ({ + data, +}: DetailWidgetProps<AdminProduct>) => { + return ( + <Container className="divide-y p-0"> + <div className="flex items-center justify-between px-6 py-4"> + <Heading level="h2"> + Product Widget {data.title} + </Heading> + </div> + </Container> + ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +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`. + +*** + +## Injection Zone + +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. + +*** + +## 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. # Admin UI Routes @@ -5536,393 +5856,75 @@ To build admin customizations that match the Medusa Admin's designs and layouts, For more customizations related to routes, refer to the [Routing Customizations chapter](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md). -# 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. - -## Why Pass Additional Data? +# Seed Data with Custom CLI Script -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. +In this chapter, you'll learn how to seed data using a custom CLI script. -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. +## How to Seed Data -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. +To seed dummy data for development or demo purposes, use a custom CLI script. -### API Routes Accepting Additional Data +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. -### API Routes List +### Example: Seed Dummy Products -- 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) +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: -## How to Pass Additional Data +```bash npm2yarn +npm install --save-dev @faker-js/faker +``` -### 1. Specify Validation of Additional Data +Then, create the file `src/scripts/demo-products.ts` with the following content: -Before passing custom data in the `additional_data` object parameter, you must specify validation rules for the allowed properties in the object. +```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" -To do that, use the middleware route object defined in `src/api/middlewares.ts`. +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, create the file `src/api/middlewares.ts` with the following content: + const defaultSalesChannel = await salesChannelModuleService + .listSalesChannels({ + name: "Default Sales Channel", + }) -```ts title="src/api/middlewares.ts" -import { defineMiddlewares } from "@medusajs/framework/http" -import { z } from "zod" + const sizeOptions = ["S", "M", "L", "XL"] + const colorOptions = ["Black", "White"] + const currency_code = "eur" + const productsNum = 50 -export default defineMiddlewares({ - routes: [ - { - method: "POST", - matcher: "/admin/products", - additionalDataValidator: { - brand: z.string().optional(), - }, - }, - ], -}) + // TODO seed products +} ``` -The middleware route object accepts an optional parameter `additionalDataValidator` whose value is an object of key-value pairs. The keys indicate the name of accepted properties in the `additional_data` parameter, and the value is [Zod](https://zod.dev/) validation rules of the property. - -In this example, you indicate that the `additional_data` parameter accepts a `brand` property whose value is an optional string. - -Refer to [Zod's documentation](https://zod.dev) for all available validation rules. - -### 2. Pass the Additional Data in a Request +So far, in the script, you: -You can now pass a `brand` property in the `additional_data` parameter of a request to the Create Product API Route. +- 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. -For example: - -```bash -curl -X POST 'http://localhost:9000/admin/products' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data '{ - "title": "Product 1", - "options": [ - { - "title": "Default option", - "values": ["Default option value"] - } - ], - "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. - -In this request, you pass in the `additional_data` parameter a `brand` property and set its value to `Acme`. - -The `additional_data` is then passed to hooks in the `createProductsWorkflow` used by the API route. - -*** - -## Use Additional Data in a Hook - -Learn about workflow hooks in [this guide](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). - -Step functions consuming the workflow hook can access the `additional_data` in the first parameter. - -For example, consider you want to store the data passed in `additional_data` in the product's `metadata` property. - -To do that, create the file `src/workflows/hooks/product-created.ts` with the following content: - -```ts title="src/workflows/hooks/product-created.ts" -import { StepResponse } from "@medusajs/framework/workflows-sdk" -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" -import { Modules } from "@medusajs/framework/utils" - -createProductsWorkflow.hooks.productsCreated( - async ({ products, additional_data }, { container }) => { - if (!additional_data?.brand) { - return - } - - const productModuleService = container.resolve( - Modules.PRODUCT - ) - - await productModuleService.upsertProducts( - products.map((product) => ({ - ...product, - metadata: { - ...product.metadata, - brand: additional_data.brand, - }, - })) - ) - - return new StepResponse(products, { - products, - additional_data, - }) - } -) -``` - -This consumes the `productsCreated` hook, which runs after the products are created. - -If `brand` is passed in `additional_data`, you resolve the Product Module's main service and use its `upsertProducts` method to update the products, adding the brand to the `metadata` property. - -### Compensation Function - -Hooks also accept a compensation function as a second parameter to undo the actions made by the step function. - -For example, pass the following second parameter to the `productsCreated` hook: - -```ts title="src/workflows/hooks/product-created.ts" -createProductsWorkflow.hooks.productsCreated( - async ({ products, additional_data }, { container }) => { - // ... - }, - async ({ products, additional_data }, { container }) => { - if (!additional_data.brand) { - return - } - - const productModuleService = container.resolve( - Modules.PRODUCT - ) - - await productModuleService.upsertProducts( - products - ) - } -) -``` - -This updates the products to their original state before adding the brand to their `metadata` property. - - -# Admin Widgets - -In this chapter, you’ll learn more about widgets and how to use them. - -## What is an Admin Widget? - -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. - -For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. - -*** - -## How to Create a Widget? - -### Prerequisites - -- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) - -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: - - - -```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" - -// The widget -const ProductWidget = () => { - return ( - <Container className="divide-y p-0"> - <div className="flex items-center justify-between px-6 py-4"> - <Heading level="h2">Product Widget</Heading> - </div> - </Container> - ) -} - -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -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. - -The widget component must be created as an arrow function. - -### Test the Widget - -To test out the widget, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, open a product’s details page. You’ll find your custom widget at the top of the page. - -*** - -## Props Passed in Detail Pages - -Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. - -For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: - -```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" -import { - DetailWidgetProps, - AdminProduct, -} from "@medusajs/framework/types" - -// The widget -const ProductWidget = ({ - data, -}: DetailWidgetProps<AdminProduct>) => { - return ( - <Container className="divide-y p-0"> - <div className="flex items-center justify-between px-6 py-4"> - <Heading level="h2"> - Product Widget {data.title} - </Heading> - </div> - </Container> - ) -} - -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -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`. - -*** - -## Injection Zone - -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. - -*** - -## 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. - - -# Seed Data with Custom CLI Script - -In this chapter, you'll learn how to seed data using a custom CLI script. - -## How to Seed Data - -To seed dummy data for development or demo purposes, use a custom CLI script. - -In the CLI script, use your custom workflows or Medusa's existing workflows, which you can browse in [this reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md), to seed data. - -### Example: Seed Dummy Products - -In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products. - -First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script: - -```bash npm2yarn -npm install --save-dev @faker-js/faker -``` - -Then, create the file `src/scripts/demo-products.ts` with the following content: - -```ts title="src/scripts/demo-products.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" -import { ExecArgs } from "@medusajs/framework/types" -import { faker } from "@faker-js/faker" -import { - ContainerRegistrationKeys, - Modules, - ProductStatus, -} from "@medusajs/framework/utils" -import { - createInventoryLevelsWorkflow, - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -export default async function seedDummyProducts({ - container, -}: ExecArgs) { - const salesChannelModuleService = container.resolve( - Modules.SALES_CHANNEL - ) - const logger = container.resolve( - ContainerRegistrationKeys.LOGGER - ) - const query = container.resolve( - ContainerRegistrationKeys.QUERY - ) - - const defaultSalesChannel = await salesChannelModuleService - .listSalesChannels({ - name: "Default Sales Channel", - }) - - const sizeOptions = ["S", "M", "L", "XL"] - const colorOptions = ["Black", "White"] - const currency_code = "eur" - const productsNum = 50 - - // TODO seed products -} -``` - -So far, in the script, you: - -- Resolve the Sales Channel Module's main service to retrieve the application's default sales channel. This is the sales channel the dummy products will be available in. -- Resolve the Logger to log messages in the terminal, and Query to later retrieve data useful for the seeded products. -- Initialize some default data to use when seeding the products next. - -Next, replace the `TODO` with the following: +Next, replace the `TODO` with the following: ```ts title="src/scripts/demo-products.ts" const productsData = new Array(productsNum).fill(0).map((_, index) => { @@ -6042,6 +6044,205 @@ npx medusa exec ./src/scripts/demo-products.ts This seeds the products to your database. If you run your Medusa application and view the products in the dashboard, you'll find fifty new products. +# 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. + +## Why Pass Additional Data? + +Some of Medusa's API Routes accept an `additional_data` parameter whose type is an object. The API Route passes the `additional_data` to the workflow, which in turn passes it to its hooks. + +This is useful when you have a link from your custom module to a commerce module, and you want to perform an additional action when a request is sent to an existing API route. + +For example, the [Create Product API Route](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. + +### API Routes Accepting Additional Data + +### API Routes List + +- 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) + +*** + +## How to Pass Additional Data + +### 1. Specify Validation of Additional Data + +Before passing custom data in the `additional_data` object parameter, you must specify validation rules for the allowed properties in the object. + +To do that, use the middleware route object defined in `src/api/middlewares.ts`. + +For example, create the file `src/api/middlewares.ts` with the following content: + +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/framework/http" +import { z } from "zod" + +export default defineMiddlewares({ + routes: [ + { + method: "POST", + matcher: "/admin/products", + additionalDataValidator: { + brand: z.string().optional(), + }, + }, + ], +}) +``` + +The middleware route object accepts an optional parameter `additionalDataValidator` whose value is an object of key-value pairs. The keys indicate the name of accepted properties in the `additional_data` parameter, and the value is [Zod](https://zod.dev/) validation rules of the property. + +In this example, you indicate that the `additional_data` parameter accepts a `brand` property whose value is an optional string. + +Refer to [Zod's documentation](https://zod.dev) for all available validation rules. + +### 2. Pass the Additional Data in a Request + +You can now pass a `brand` property in the `additional_data` parameter of a request to the Create Product API Route. + +For example: + +```bash +curl -X POST 'http://localhost:9000/admin/products' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "title": "Product 1", + "options": [ + { + "title": "Default option", + "values": ["Default option value"] + } + ], + "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. + +In this request, you pass in the `additional_data` parameter a `brand` property and set its value to `Acme`. + +The `additional_data` is then passed to hooks in the `createProductsWorkflow` used by the API route. + +*** + +## Use Additional Data in a Hook + +Learn about workflow hooks in [this guide](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). + +Step functions consuming the workflow hook can access the `additional_data` in the first parameter. + +For example, consider you want to store the data passed in `additional_data` in the product's `metadata` property. + +To do that, create the file `src/workflows/hooks/product-created.ts` with the following content: + +```ts title="src/workflows/hooks/product-created.ts" +import { StepResponse } from "@medusajs/framework/workflows-sdk" +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +import { Modules } from "@medusajs/framework/utils" + +createProductsWorkflow.hooks.productsCreated( + async ({ products, additional_data }, { container }) => { + if (!additional_data?.brand) { + return + } + + const productModuleService = container.resolve( + Modules.PRODUCT + ) + + await productModuleService.upsertProducts( + products.map((product) => ({ + ...product, + metadata: { + ...product.metadata, + brand: additional_data.brand, + }, + })) + ) + + return new StepResponse(products, { + products, + additional_data, + }) + } +) +``` + +This consumes the `productsCreated` hook, which runs after the products are created. + +If `brand` is passed in `additional_data`, you resolve the Product Module's main service and use its `upsertProducts` method to update the products, adding the brand to the `metadata` property. + +### Compensation Function + +Hooks also accept a compensation function as a second parameter to undo the actions made by the step function. + +For example, pass the following second parameter to the `productsCreated` hook: + +```ts title="src/workflows/hooks/product-created.ts" +createProductsWorkflow.hooks.productsCreated( + async ({ products, additional_data }, { container }) => { + // ... + }, + async ({ products, additional_data }, { container }) => { + if (!additional_data.brand) { + return + } + + const productModuleService = container.resolve( + Modules.PRODUCT + ) + + await productModuleService.upsertProducts( + products + ) + } +) +``` + +This updates the products to their original state before adding the brand to their `metadata` property. + + # Handling CORS in API Routes In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes. @@ -6154,286 +6355,36 @@ export default defineMiddlewares({ This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`. -# HTTP Methods +# Throwing and Handling Errors -In this chapter, you'll learn about how to add new API routes for each HTTP method. +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. -## HTTP Method Handler +## Throw MedusaError -An API route is created for every HTTP method you export a handler function for in a route file. +When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError` from the Medusa Framework. -Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`. +The Medusa application's API route error handler then wraps your thrown error in a uniform object and returns it in the response. -For example, create the file `src/api/hello-world/route.ts` with the following content: +For example: -```ts title="src/api/hello-world/route.ts" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +```ts +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { MedusaError } from "@medusajs/framework/utils" export const GET = async ( req: MedusaRequest, res: MedusaResponse ) => { - res.json({ - message: "[GET] Hello world!", - }) + if (!req.query.q) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "The `q` query parameter is required." + ) + } + + // ... } - -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "[POST] Hello world!", - }) -} -``` - -This adds two API Routes: - -- A `GET` route at `http://localhost:9000/hello-world`. -- A `POST` route at `http://localhost:9000/hello-world`. - - -# Middlewares - -In this chapter, you’ll learn about middlewares and how to create them. - -## What is a Middleware? - -A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler function. - -Middlwares are used to guard API routes, parse request content types other than `application/json`, manipulate request data, and more. - -As Medusa's server is based on Express, you can use any [Express middleware](https://expressjs.com/en/resources/middleware.html). - -*** - -## How to Create a Middleware? - -Middlewares 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. - -For example: - -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - MedusaNextFunction, - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom*", - middlewares: [ - ( - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - console.log("Received a request!") - - next() - }, - ], - }, - ], -}) -``` - -The `defineMiddlewares` function accepts a middleware configurations object that has the property `routes`. `routes`'s value is an array of middleware route objects, each having the following properties: - -- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. The regular expression must be compatible with [path-to-regexp](https://github.com/pillarjs/path-to-regexp). -- `middlewares`: An array of middleware functions. - -In the example above, you define a middleware that logs the message `Received a request!` whenever a request is sent to an API route path starting with `/custom`. - -*** - -## Test the Middleware - -To test the middleware: - -1. Start the application: - -```bash npm2yarn -npm run dev -``` - -2. Send a request to any API route starting with `/custom`. -3. See the following message in the terminal: - -```bash -Received a request! -``` - -*** - -## When to Use Middlewares - -- You want to protect API routes by a custom condition. -- You're modifying the request body. - -*** - -## Middleware Function Parameters - -The middleware function accepts three parameters: - -1. A request object of type `MedusaRequest`. -2. A response object of type `MedusaResponse`. -3. A function of type `MedusaNextFunction` that executes the next middleware in the stack. - -You must call the `next` function in the middleware. Otherwise, other middlewares and the API route handler won’t execute. - -*** - -## Middleware for Routes with Path Parameters - -To indicate a path parameter in a middleware's `matcher` pattern, use the format `:{param-name}`. - -For example: - -```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports" highlights={pathParamHighlights} -import { - MedusaNextFunction, - MedusaRequest, - MedusaResponse, - defineMiddlewares, -} from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom/:id", - middlewares: [ - // ... - ], - }, - ], -}) -``` - -This applies a middleware to the routes defined in the file `src/api/custom/[id]/route.ts`. - -*** - -## Restrict HTTP Methods - -Restrict which HTTP methods the middleware is applied to using the `method` property of the middleware route object. - -For example: - -```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - MedusaNextFunction, - MedusaRequest, - MedusaResponse, - defineMiddlewares, -} from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom*", - method: ["POST", "PUT"], - middlewares: [ - // ... - ], - }, - ], -}) -``` - -`method`'s value is one or more HTTP methods to apply the middleware to. - -This example applies the middleware only when a `POST` or `PUT` request is sent to an API route path starting with `/custom`. - -*** - -## Request URLs with Trailing Backslashes - -A middleware whose `matcher` pattern doesn't end with a backslash won't be applied for requests to URLs with a trailing backslash. - -For example, consider you have the following middleware: - -```ts collapsibleLines="1-7" expandMoreLabel="Show Imports" -import { - MedusaNextFunction, - MedusaRequest, - MedusaResponse, - defineMiddlewares, -} from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom", - middlewares: [ - ( - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - console.log("Received a request!") - - next() - }, - ], - }, - ], -}) -``` - -If you send a request to `http://localhost:9000/custom`, the middleware will run. - -However, if you send a request to `http://localhost:9000/custom/`, the middleware won't run. - -In general, avoid adding trailing backslashes when sending requests to API routes. - -*** - -## Middlewares Precedence in Registration - -The Medusa application registers your middlewares first, then registers middlewares defined in Medusa's core. - -So, if you add a middleware for a route defined in the core, it might get overridden by the core middleware. For example, if you add a middleware to change authentication of admin routes, the authentication middleware defined in the core will still run, leading to your middleware not being effective. - - -# Throwing and Handling Errors - -In this guide, you'll learn how to throw errors in your Medusa application, how it affects an API route's response, and how to change the default error handler of your Medusa application. - -## Throw MedusaError - -When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError` from the Medusa Framework. - -The Medusa application's API route error handler then wraps your thrown error in a uniform object and returns it in the response. - -For example: - -```ts -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { MedusaError } from "@medusajs/framework/utils" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - if (!req.query.q) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "The `q` query parameter is required." - ) - } - - // ... -} -``` +``` The `MedusaError` class accepts in its constructor two parameters: @@ -6511,69 +6462,19 @@ The `errorHandler` property's value is a function that accepts four parameters: This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message. -# API Route Parameters - -In this chapter, you’ll learn about path, query, and request body parameters. - -## Path Parameters - -To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`. - -For example, to create an API Route at the path `/hello-world/:id`, where `:id` is a path parameter, create the file `src/api/hello-world/[id]/route.ts` with the following content: - -```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[GET] Hello ${req.params.id}!`, - }) -} -``` - -The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs. - -### Multiple Path Parameters - -To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`. - -For example, to create an API route at `/hello-world/:id/name/:name`, create the file `src/api/hello-world/[id]/name/[name]/route.ts` with the following content: - -```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[GET] Hello ${ - req.params.id - } - ${req.params.name}!`, - }) -} -``` +# HTTP Methods -You access the `id` and `name` path parameters using the `req.params` property. +In this chapter, you'll learn about how to add new API routes for each HTTP method. -*** +## HTTP Method Handler -## Query Parameters +An API route is created for every HTTP method you export a handler function for in a route file. -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. +Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`. -For example: +For example, create the file `src/api/hello-world/route.ts` with the following content: -```ts title="src/api/hello-world/route.ts" highlights={queryHighlights} +```ts title="src/api/hello-world/route.ts" import type { MedusaRequest, MedusaResponse, @@ -6584,76 +6485,24 @@ export const GET = async ( res: MedusaResponse ) => { res.json({ - message: `Hello ${req.query.name}`, + message: "[GET] Hello world!", }) } -``` - -The value of `req.query.name` is the value passed in `?name=John`, for example. - -### Validate Query Parameters - -You can apply validation rules on received query 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-query-paramters/index.html.md). - -*** - -## Request Body Parameters - -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. - -Learn more about configuring body parsing in [this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md). - -For example: - -```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -type HelloWorldReq = { - name: string -} export const POST = async ( - req: MedusaRequest<HelloWorldReq>, + req: MedusaRequest, res: MedusaResponse ) => { res.json({ - message: `[POST] Hello ${req.body.name}!`, + message: "[POST] Hello world!", }) } ``` -In this example, you use the `name` request body parameter to create the message in the returned response. - -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: - -```bash -curl -X POST 'http://localhost:9000/hello-world' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "name": "John" -}' -``` - -This returns the following JSON object: - -```json -{ - "message": "[POST] Hello John!" -} -``` - -### Validate Body Parameters - -You can apply validation rules on received body parameters to ensure they match specified rules and types. +This adds two API Routes: -Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md). +- A `GET` route at `http://localhost:9000/hello-world`. +- A `POST` route at `http://localhost:9000/hello-world`. # Configure Request Body Parser @@ -6987,132 +6836,521 @@ export const GET = async ( In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user. -# API Route Response +# API Route Parameters -In this chapter, you'll learn how to send a response in your API route. +In this chapter, you’ll learn about path, query, and request body parameters. -## Send a JSON Response +## Path Parameters -To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler. +To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`. -For example: +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/custom/route.ts" highlights={jsonHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" export const GET = async ( req: MedusaRequest, res: MedusaResponse ) => { res.json({ - message: "Hello, World!", + message: `[GET] Hello ${req.params.id}!`, }) } ``` -This API route returns the following JSON object: - -```json -{ - "message": "Hello, World!" -} -``` - -*** - -## Set Response Status Code +The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs. -By default, setting the JSON data using the `json` method returns a response with a `200` status code. +### Multiple Path Parameters -To change the status code, use the `status` method of the `MedusaResponse` object. +To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`. -For example: +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: -```ts title="src/api/custom/route.ts" highlights={statusHighlight} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" export const GET = async ( req: MedusaRequest, res: MedusaResponse ) => { - res.status(201).json({ - message: "Hello, World!", + res.json({ + message: `[GET] Hello ${ + req.params.id + } - ${req.params.name}!`, }) } ``` -The response of this API route has the status code `201`. +You access the `id` and `name` path parameters using the `req.params` property. *** -## Change Response Content Type +## Query Parameters -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. +You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value. -For example, to create an API route that returns an event stream: +For example: -```ts highlights={streamHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +```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.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }) - - const interval = setInterval(() => { - res.write("Streaming data...\n") - }, 3000) - - req.on("end", () => { - clearInterval(interval) - res.end() + res.json({ + message: `Hello ${req.query.name}`, }) } ``` -The `writeHead` method accepts two parameters: +The value of `req.query.name` is the value passed in `?name=John`, for example. -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. +### Validate Query Parameters -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. +You can apply validation rules on received query 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-query-paramters/index.html.md). *** -## Do More with Responses +## Request Body Parameters -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. +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. +Learn more about configuring body parsing in [this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md). -# Request Body and Query Parameter Validation +For example: -In this chapter, you'll learn how to validate request body and query parameters in your custom API route. +```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -## Request Validation +type HelloWorldReq = { + name: string +} -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. +export const POST = async ( + req: MedusaRequest<HelloWorldReq>, + res: MedusaResponse +) => { + res.json({ + message: `[POST] Hello ${req.body.name}!`, + }) +} +``` -Medusa provides two middlewares to validate the request body and query paramters of incoming requests to your custom API routes: +In this example, you use the `name` request body parameter to create the message in the returned response. -- `validateAndTransformBody` to validate the request's body parameters against a schema. -- `validateAndTransformQuery` to validate the request's query parameters against a schema. +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. -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. +To test it out, send the following request to your Medusa application: -The next steps explain how to add request body and query parameter validation to the API route mentioned earlier. +```bash +curl -X POST 'http://localhost:9000/hello-world' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "name": "John" +}' +``` -*** +This returns the following JSON object: -## How to Validate Request Body +```json +{ + "message": "[POST] Hello John!" +} +``` -### Step 1: Create Validation Schema +### Validate Body Parameters -Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. +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). + + +# Middlewares + +In this chapter, you’ll learn about middlewares and how to create them. + +## What is a Middleware? + +A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler function. + +Middlwares are used to guard API routes, parse request content types other than `application/json`, manipulate request data, and more. + +As Medusa's server is based on Express, you can use any [Express middleware](https://expressjs.com/en/resources/middleware.html). + +### Middleware Types + +There are two types of middlewares: + +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). + +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. + +*** + +## How to Create a Global Middleware? + +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. + +For example: + +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + MedusaNextFunction, + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom*", + middlewares: [ + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + console.log("Received a request!") + + next() + }, + ], + }, + ], +}) +``` + +The `defineMiddlewares` function accepts a middleware configurations object that has the property `routes`. `routes`'s value is an array of middleware route objects, each having the following properties: + +- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. The regular expression must be compatible with [path-to-regexp](https://github.com/pillarjs/path-to-regexp). +- `middlewares`: An array of middleware functions. + +In the example above, you define a 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 + +To test the middleware: + +1. Start the application: + +```bash npm2yarn +npm run dev +``` + +2. Send a request to any API route starting with `/custom`. +3. See the following message in the terminal: + +```bash +Received a request! +``` + +*** + +## How to Create a Route Middleware? + +In the previous section, you learned how to create a global middleware. You define the route middleware in the same way, but you specify an additional property `method`. Its value is one or more HTTP methods to apply the middleware to. + +For example: + +```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, + defineMiddlewares, +} 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!") + + next() + }, + ], + }, + ], +}) +``` + +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 + +To test the middleware: + +1. Start the application: + +```bash npm2yarn +npm run dev +``` + +2. Send a `POST` request to any API route starting with `/custom`. +3. See the following message in the terminal: + +```bash +Received a request! +``` + +*** + +## When to Use Middlewares + +- You want to protect API routes by a custom condition. +- You're modifying the request body. + +*** + +## Middleware Function Parameters + +The middleware function accepts three parameters: + +1. A request object of type `MedusaRequest`. +2. A response object of type `MedusaResponse`. +3. A function of type `MedusaNextFunction` that executes the next middleware in the stack. + +You must call the `next` function in the middleware. Otherwise, other middlewares and the API route handler won’t execute. + +*** + +## Middleware for Routes with Path Parameters + +To indicate a path parameter in a middleware's `matcher` pattern, use the format `:{param-name}`. + +For example: + +```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports" highlights={pathParamHighlights} +import { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, + defineMiddlewares, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/:id", + middlewares: [ + // ... + ], + }, + ], +}) +``` + +This applies a middleware to the routes defined in the file `src/api/custom/[id]/route.ts`. + +*** + +## Request URLs with Trailing Backslashes + +A middleware whose `matcher` pattern doesn't end with a backslash won't be applied for requests to URLs with a trailing backslash. + +For example, consider you have the following middleware: + +```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports" +import { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, + defineMiddlewares, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom", + middlewares: [ + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + console.log("Received a request!") + + next() + }, + ], + }, + ], +}) +``` + +If you send a request to `http://localhost:9000/custom`, the middleware will run. + +However, if you send a request to `http://localhost:9000/custom/`, the middleware won't run. + +In general, avoid adding trailing backslashes when sending requests to API routes. + +*** + +## 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) + +The Medusa application registers middlewares and API route handlers in the following order: + +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. + +### Middlewares Sorting + +On top of the previous ordering, Medusa sorts global and route middlewares based on their matcher pattern in the following order: + +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`. + +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 global middlewares are sorted into the following order before they're registered: + +1. Global middleware `/custom`. +2. Global middleware `/custom/:id`. + +And the route middlewares are sorted into the following order before they're registered: + +1. Route middleware `/custom*`. +2. Route middleware `/custom/:id`. + +### Middlewares and Route Execution Order + +When a request is sent to an API route, the global middlewares are executed first, then the route middlewares, and finally the route handler. + +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 +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!`. + +*** + +## Overriding Middlewares + +A middleware can never override an existing middleware. Instead, middlewares are added to the end of the middleware stack. + +For example, if you define a custom validation middleware on an existing route, such as `validateAndTransformBody`, then both the original and the custom validation middleware will run. + + +# 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 + +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. + +Medusa provides two middlewares to validate the request body and query paramters of incoming requests to your custom API routes: + +- `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. + +The next steps explain how to add request body and query parameter validation to the API route mentioned earlier. + +*** + +## How to Validate Request Body + +### Step 1: Create Validation Schema + +Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. 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. @@ -7338,121 +7576,106 @@ For example, if you omit the `a` parameter, you'll receive a `400` response code To see different examples and learn more about creating a validation schema, refer to [Zod's documentation](https://zod.dev). -# Event Data Payload - -In this chapter, you'll learn how subscribers receive an event's data payload. +# API Route Response -## Access Event's Data Payload +In this chapter, you'll learn how to send a response in your API route. -When events are emitted, they’re emitted with a data payload. +## Send a JSON Response -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. +To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler. For example: -```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" - -export default async function productCreateHandler({ - event, -}: SubscriberArgs<{ id: string }>) { - const productId = event.data.id - console.log(`The product ${productId} was created`) -} +```ts title="src/api/custom/route.ts" highlights={jsonHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -export const config: SubscriberConfig = { - event: "product.created", +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "Hello, World!", + }) } ``` -The `event` object has the following properties: - -- 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. - -This logs the product ID received in the `product.created` event’s data payload to the console. +This API route returns the following JSON object: -{/* --- +```json +{ + "message": "Hello, World!" +} +``` -## 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. */} +## Set Response Status Code +By default, setting the JSON data using the `json` method returns a response with a `200` status code. -# Add Data Model Check Constraints +To change the status code, use the `status` method of the `MedusaResponse` object. -In this chapter, you'll learn how to add check constraints to your data model. +For example: -## What is a Check Constraint? +```ts title="src/api/custom/route.ts" highlights={statusHighlight} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown. +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.status(201).json({ + message: "Hello, World!", + }) +} +``` -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. +The response of this API route has the status code `201`. *** -## How to Set a Check Constraint? - -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" +## Change Response Content Type -const CustomProduct = model.define("custom_product", { - // ... - price: model.bigNumber(), -}) -.checks([ - (columns) => `${columns.price} >= 0`, -]) -``` +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. -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. +For example, to create an API route that returns an event stream: -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. +```ts highlights={streamHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -You can also pass an object to the `checks` method: +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }) -```ts highlights={checks2Highlights} -import { model } from "@medusajs/framework/utils" + const interval = setInterval(() => { + res.write("Streaming data...\n") + }, 3000) -const CustomProduct = model.define("custom_product", { - // ... - price: model.bigNumber(), -}) -.checks([ - { - name: "custom_product_price_check", - expression: (columns) => `${columns.price} >= 0`, - }, -]) + req.on("end", () => { + clearInterval(interval) + res.end() + }) +} ``` -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). - -*** +The `writeHead` method accepts two parameters: -## Apply in Migrations +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. -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. +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. -To generate a migration for the data model's module then reflect it on the database, run the following command: +*** -```bash -npx medusa db:generate custom_module -npx medusa db:migrate -``` +## Do More with Responses -The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. +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. # Emit Workflow and Service Events @@ -7623,2436 +7846,2553 @@ If you execute the `performAction` method of your service, the event is emitted Any subscribers listening to the event are also executed. -# Configure Data Model Properties +# Event Data Payload -In this chapter, you’ll learn how to configure data model properties. +In this chapter, you'll learn how subscribers receive an event's data payload. -## Property’s Default Value +## Access Event's Data Payload -Use the `default` method on a property's definition to specify the default value of a property. +When events are emitted, they’re emitted with a data payload. + +The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context. For example: -```ts highlights={defaultHighlights} -import { model } from "@medusajs/framework/utils" +```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" -const MyCustom = model.define("my_custom", { - color: model - .enum(["black", "white"]) - .default("black"), - age: model - .number() - .default(0), - // ... -}) +export default async function productCreateHandler({ + event, +}: SubscriberArgs<{ id: string }>) { + const productId = event.data.id + console.log(`The product ${productId} was created`) +} -export default MyCustom +export const config: SubscriberConfig = { + event: "product.created", +} ``` -In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`. +The `event` object has the following properties: -*** +- 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. -## Nullable Property +This logs the product ID received in the `product.created` event’s data payload to the console. -Use the `nullable` method to indicate that a property’s value can be `null`. +{/* --- -For example: +## List of Events with Data Payload -```ts highlights={nullableHighlights} -import { model } from "@medusajs/framework/utils" +Refer to [this reference](!resources!/events-reference) for a full list of events emitted by Medusa and their data payloads. */} -const MyCustom = model.define("my_custom", { - price: model.bigNumber().nullable(), - // ... -}) -export default MyCustom -``` - -*** +# Add Columns to a Link -## Unique Property +In this chapter, you'll learn how to add custom columns to a link definition and manage them. -The `unique` method indicates that a property’s value must be unique in the database through a unique index. +## How to Add Custom Columns to a Link's Table? -For example: +The `defineLink` function used to define a link accepts a third parameter, which is an object of options. -```ts highlights={uniqueHighlights} -import { model } from "@medusajs/framework/utils" +To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property: -const User = model.define("user", { - email: model.text().unique(), - // ... -}) +```ts highlights={linkHighlights} +import HelloModule from "../modules/hello" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" -export default User +export default defineLink( + ProductModule.linkable.product, + HelloModule.linkable.myCustom, + { + database: { + extraColumns: { + metadata: { + type: "json", + }, + }, + }, + } +) ``` -In this example, multiple users can’t have the same email. - - -# Data Model Database Index - -In this chapter, you’ll learn how to define a database index on a data model. - -## Define Database Index on Property +This adds to the table created for the link between `product` and `myCustom` a `metadata` column of type `json`. -Use the `index` method on a property's definition to define a database index. +### Database Options -For example: +The `database` property defines configuration for the table created in the database. -```ts highlights={highlights} -import { model } from "@medusajs/framework/utils" +Its `extraColumns` property defines custom columns to create in the link's table. -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text().index( - "IDX_MY_CUSTOM_NAME" - ), -}) +`extraColumns`'s value is an object whose keys are the names of the columns, and values are the column's configurations as an object. -export default MyCustom -``` +### Column Configurations -The `index` method optionally accepts the name of the index as a parameter. +The column's configurations object accepts the following properties: -In this example, you define an index on the `name` property. +- `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. *** -## Define Database Index on Data Model +## Set Custom Column when Creating Link -A data model has an `indexes` method that defines database indices on its properties. +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. -The index can be on multiple columns (composite index). For example: +For example: -```ts highlights={dataModelIndexHighlights} -import { model } from "@medusajs/framework/utils" +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 MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], +```ts +await link.create({ + [Modules.PRODUCT]: { + product_id: "123", }, -]) - -export default MyCustom + HELLO_MODULE: { + my_custom_id: "321", + }, + data: { + metadata: { + test: true, + }, + }, +}) ``` -The `indexes` method receives an array of indices as a parameter. Each index is an object with a required `on` property indicating the properties to apply the index on. +*** -In the above example, you define a composite index on the `name` and `age` properties. +## Retrieve Custom Column with Link -### Index Conditions +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. -An index can have conditions. For example: +For example: -```ts highlights={conditionHighlights} -import { model } from "@medusajs/framework/utils" +```ts highlights={retrieveHighlights} +import productHelloLink from "../links/product-hello" -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - where: { - age: 30, - }, - }, -]) +// ... -export default MyCustom +const { data } = await query.graph({ + entity: productHelloLink.entryPoint, + fields: ["metadata", "product.*", "my_custom.*"], + filters: { + product_id: "prod_123", + }, +}) ``` -The index object passed to `indexes` accepts a `where` property whose value is an object of conditions. The object's key is a property's name, and its value is the condition on that property. - -In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`. - -A property's condition can be a negation. For example: - -```ts highlights={negationHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number().nullable(), -}).indexes([ - { - on: ["name", "age"], - where: { - age: { - $ne: null, - }, - }, - }, -]) +This retrieves the product of id `prod_123` and its linked `my_custom` records. -export default MyCustom -``` +In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link. -A property's value in `where` can be an object having a `$ne` property. `$ne`'s value indicates what the specified property's value shouldn't be. +*** -In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`. +## Update Custom Column's Value -### Unique Database Index +Link's `create` method updates a link's data if the link between the specified records already exists. -The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index. +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 highlights={uniqueHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - unique: true, +```ts +await link.create({ + [Modules.PRODUCT]: { + product_id: "123", }, -]) - -export default MyCustom + HELLO_MODULE: { + my_custom_id: "321", + }, + data: { + metadata: { + test: false, + }, + }, +}) ``` -This creates a unique composite index on the `name` and `age` properties. +# Module Link Direction -# Infer Type of Data Model +In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case. -In this chapter, you'll learn how to infer the type of a data model. +## Link Direction -## How to Infer Type of Data Model? +The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`. -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. +For example, the following defines a link from the `helloModuleService`'s `myCustom` data model to the Product Module's `product` data model: -Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. +```ts +export default defineLink( + HelloModule.linkable.myCustom, + ProductModule.linkable.product +) +``` -For example: +Whereas the following defines a link from the Product Module's `product` data model to the `helloModuleService`'s `myCustom` data model: ```ts -import { InferTypeOf } from "@medusajs/framework/types" -import { MyCustom } from "../models/my-custom" // relative path to the model - -export type MyCustom = InferTypeOf<typeof MyCustom> +export default defineLink( + ProductModule.linkable.product, + HelloModule.linkable.myCustom +) ``` -`InferTypeOf` accepts as a type argument the type of the data model. +The above links are two different links that serve different purposes. -Since the `MyCustom` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. +*** -You can now use the `MyCustom` type to reference a data model in other types, such as in workflow inputs or service method outputs: +## Which Link Direction to Use? -```ts title="Example Service" -// other imports... -import { InferTypeOf } from "@medusajs/framework/types" -import { MyCustom } from "../models/my-custom" +### Extend Data Models -type MyCustom = InferTypeOf<typeof MyCustom> +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. -class HelloModuleService extends MedusaService({ MyCustom }) { - async doSomething(): Promise<MyCustom> { - // ... - } -} +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 +) ``` +### Associate Data Models -# Data Model Default Properties +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. -In this chapter, you'll learn about the properties available by default in your 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`: -When you create a data model, the following properties are created for you by Medusa: +```ts +export default defineLink( + HelloModule.linkable.post, + ProductModule.linkable.product +) +``` -- `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. +# Link -# Data Model’s Primary Key +In this chapter, you’ll learn what Link is and how to use it to manage links. -In this chapter, you’ll learn how to configure the primary key of a data model. +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. -## primaryKey Method +## What is Link? -To set any `id`, `text`, or `number` property as a primary key, use the `primaryKey` method. +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 highlights={highlights} -import { model } from "@medusajs/framework/utils" +```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), +export async function POST( + req: MedusaRequest, + res: MedusaResponse +): Promise<void> { + const link = req.scope.resolve( + ContainerRegistrationKeys.LINK + ) + // ... -}) - -export default MyCustom +} ``` -In the example above, the `id` property is defined as the data model's primary key. - - -# Data Model Property Types +You can use its methods to manage links, such as create or delete links. -In this chapter, you’ll learn about the types of properties in a data model’s schema. +*** -## id +## Create Link -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. +To create a link between records of two data models, use the `create` method of Link. For example: -```ts highlights={idHighlights} -import { model } from "@medusajs/framework/utils" +```ts +import { Modules } from "@medusajs/framework/utils" -const MyCustom = model.define("my_custom", { - id: model.id(), - // ... -}) +// ... -export default MyCustom +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + "helloModuleService": { + my_custom_id: "mc_123", + }, +}) ``` +The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules. + +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 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. + *** -## text +## Dismiss Link -The `text` method defines a string property. +To remove a link between records of two data models, use the `dismiss` method of Link. For example: -```ts highlights={textHighlights} -import { model } from "@medusajs/framework/utils" +```ts +import { Modules } from "@medusajs/framework/utils" -const MyCustom = model.define("my_custom", { - name: model.text(), - // ... -}) +// ... -export default MyCustom +await link.dismiss({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + "helloModuleService": { + my_custom_id: "mc_123", + }, +}) ``` +The `dismiss` method accepts the same parameter type as the [create method](#create-link). + +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. + *** -## number +## Cascade Delete Linked Records -The `number` method defines a number property. +If a record is deleted, use the `delete` method of Link to delete all linked records. For example: -```ts highlights={numberHighlights} -import { model } from "@medusajs/framework/utils" +```ts +import { Modules } from "@medusajs/framework/utils" -const MyCustom = model.define("my_custom", { - age: model.number(), - // ... -}) +// ... -export default MyCustom +await productModuleService.deleteVariants([variant.id]) + +await link.delete({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, +}) ``` +This deletes all records linked to the deleted product. + *** -## float +## Restore Linked Records -This property is only available after [Medusa v2.1.2](https://github.com/medusajs/medusa/releases/tag/v2.1.2). +If a record that was previously soft-deleted is now restored, use the `restore` method of Link to restore all linked records. -The `float` method defines a number property that allows for values with decimal places. +For example: -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). +```ts +import { Modules } from "@medusajs/framework/utils" -For example: +// ... -```ts highlights={floatHighlights} -import { model } from "@medusajs/framework/utils" +await productModuleService.restoreProducts(["prod_123"]) -const MyCustom = model.define("my_custom", { - rating: model.float(), - // ... +await link.restore({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, }) - -export default MyCustom ``` -*** - -## bigNumber - -The `bigNumber` method defines a number property that expects large numbers, such as prices. -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). +# Query -For example: +In this chapter, you’ll learn about Query and how to use it to fetch data from modules. -```ts highlights={bigNumberHighlights} -import { model } from "@medusajs/framework/utils" +## What is Query? -const MyCustom = model.define("my_custom", { - price: model.bigNumber(), - // ... -}) +Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key. -export default MyCustom -``` +In your resources, such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s commerce modules. *** -## boolean +## Query Example -The `boolean` method defines a boolean property. +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={booleanHighlights} -import { model } from "@medusajs/framework/utils" +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) -const MyCustom = model.define("my_custom", { - hasAccount: model.boolean(), - // ... -}) + const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + }) -export default MyCustom + res.json({ my_customs: myCustoms }) +} ``` -*** +In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key. -### enum +Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties: -The `enum` method defines a property whose value can only be one of the specified values. +- `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. -For example: +The method returns an object that has a `data` property, which holds an array of the retrieved data. For example: -```ts highlights={enumHighlights} -import { model } from "@medusajs/framework/utils" +```json title="Returned Data" +{ + "data": [ + { + "id": "123", + "name": "test" + } + ] +} +``` -const MyCustom = model.define("my_custom", { - color: model.enum(["black", "white"]), - // ... -}) +*** -export default MyCustom -``` +## Querying the Graph -The `enum` method accepts an array of possible string values. +When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates. + +This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them. *** -## dateTime +## Retrieve Linked Records -The `dateTime` method defines a timestamp property. +Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`. For example: -```ts highlights={dateTimeHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - date_of_birth: model.dateTime(), - // ... +```ts highlights={[["6"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: [ + "id", + "name", + "product.*", + ], }) - -export default MyCustom ``` -*** +`.*` 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`. -## json +### Retrieve List Link Records -The `json` method defines a property whose value is a stringified JSON object. +If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`. For example: -```ts highlights={jsonHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - metadata: model.json(), - // ... +```ts highlights={[["6"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: [ + "id", + "name", + "products.*", + ], }) - -export default MyCustom ``` -*** +### Apply Filters and Pagination on Linked Records -## array +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 `array` method defines an array of strings property. +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. + +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 highlights={arrHightlights} -import { model } from "@medusajs/framework/utils" +```ts highlights={queryLinkTableHighlights} +import productCustomLink from "../../../links/product-custom" -const MyCustom = model.define("my_custom", { - names: model.array(), - // ... -}) +// ... -export default MyCustom +const { data: productCustoms } = await query.graph({ + entity: productCustomLink.entryPoint, + fields: ["*", "product.*", "my_custom.*"], + pagination: { + take: 5, + skip: 0, + }, +}) ``` -*** - -## Properties Reference - -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. - - -# Manage Relationships +In the object passed to the `graph` method: -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. +- 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. -## Manage One-to-One Relationship +You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination). -### BelongsTo Side of One-to-One +The returned `data` is similar to the following: -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. +```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, 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: +*** -```ts highlights={belongsHighlights} -// when creating an email -const email = await helloModuleService.createEmails({ - // other properties... - user_id: "123", -}) +## Apply Filters -// when updating an email -const email = await helloModuleService.updateEmails({ - id: "321", - // other properties... - user_id: "123", +```ts highlights={[["6"], ["7"], ["8"], ["9"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + filters: { + id: [ + "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX", + "mc_01HWSVWK3KYHKQEE6QGS2JC3FX", + ], + }, }) ``` -In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to. +The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records. -### HasOne Side +In the example above, you filter the `my_custom` records by multiple IDs. -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. +Filters don't apply on fields of linked data models from other modules. -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: +*** -```ts highlights={hasOneHighlights} -// when creating a user -const user = await helloModuleService.createUsers({ - // other properties... - email: "123", -}) +## Apply Pagination -// when updating a user -const user = await helloModuleService.updateUsers({ - id: "321", - // other properties... - email: "123", +```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, + }, }) ``` -In the example above, you pass the `email` property when creating or updating a user to specify the email it has. - -*** +The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records. -## Manage One-to-Many Relationship +To paginate the returned records, pass the following properties to `pagination`: -In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. +- `skip`: (required to apply pagination) The number of records to skip before fetching the results. +- `take`: The number of records to fetch. -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. +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: -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: +- skip: (\`number\`) The number of records skipped. +- take: (\`number\`) The number of records requested to fetch. +- count: (\`number\`) The total number of records. -```ts highlights={manyBelongsHighlights} -// when creating a product -const product = await helloModuleService.createProducts({ - // other properties... - store_id: "123", -}) +### Sort Records -// when updating a product -const product = await helloModuleService.updateProducts({ - id: "321", - // other properties... - store_id: "123", +```ts highlights={[["5"], ["6"], ["7"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + pagination: { + order: { + name: "DESC", + }, + }, }) ``` -In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to. - -*** - -## Manage Many-to-Many Relationship - -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 - -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. +Sorting doesn't work on fields of linked data models from other modules. -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: +To sort returned records, pass an `order` property to `pagination`. -```ts highlights={manyHighlights} -// when creating a product -const product = await helloModuleService.createProducts({ - // other properties... - orders: ["123", "321"], -}) +The `order` property is an object whose keys are property names, and values are either: -// when creating an order -const order = await helloModuleService.createOrders({ - id: "321", - // other properties... - products: ["123", "321"], -}) -``` +- `ASC` to sort records by that property in ascending order. +- `DESC` to sort records by that property in descending order. -In the example above, you pass the `orders` property when you create a product, and you pass the `products` property when you create an order. +*** -### Update Associations +## Request Query Configurations -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. +For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that: -However, this removes any existing associations to records whose IDs aren't included in the array. +- 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. -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: +Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request. -```ts -const product = await helloModuleService.updateProducts({ - id: "123", - // other properties... - orders: ["321"], -}) -``` +### Step 1: Add Middleware -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. +The first step is to use the `validateAndTransformQuery` middleware on the `GET` route. You add the middleware in `src/api/middlewares.ts`: -So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product: +```ts title="src/api/middlewares.ts" +import { + validateAndTransformQuery, + defineMiddlewares, +} from "@medusajs/framework/http" +import { createFindParams } from "@medusajs/medusa/api/utils/validators" -```ts highlights={updateAssociationHighlights} -const product = await helloModuleService.retrieveProduct( - "123", - { - relations: ["orders"], - } -) +export const GetCustomSchema = createFindParams() -const updatedProduct = await helloModuleService.updateProducts({ - id: product.id, - // other properties... - orders: [ - ...product.orders.map((order) => order.id), - "321", +export default defineMiddlewares({ + routes: [ + { + matcher: "/customs", + method: "GET", + middlewares: [ + validateAndTransformQuery( + GetCustomSchema, + { + defaults: [ + "id", + "name", + "products.*", + ], + isList: true, + } + ), + ], + }, ], }) ``` -This keeps existing associations between the product and orders, and adds a new one. - -*** +The `validateAndTransformQuery` accepts two parameters: -## Manage Many-to-Many Relationship with pivotEntity +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`. -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. +### Step 2: Use Configurations in API Route -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. +After applying this middleware, your API route now accepts the `fields`, `offset`, `limit`, and `order` query parameters mentioned above. -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: +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={["4"]} -class HelloModuleService extends MedusaService({ - Order, - Product, - OrderProduct, -}) {} -``` +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. -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, Create the file `src/api/customs/route.ts` with the following content: -For example: +```ts title="src/api/customs/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -```ts -// create order-product association -const orderProduct = await helloModuleService.createOrderProducts({ - order_id: "123", - product_id: "123", - metadata: { - test: true, - }, -}) +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) -// update order-product association -const orderProduct = await helloModuleService.updateOrderProducts({ - id: "123", - metadata: { - test: false, - }, -}) + const { data: myCustoms } = await query.graph({ + entity: "my_custom", + ...req.queryConfig, + }) -// delete order-product association -await helloModuleService.deleteOrderProducts("123") + res.json({ my_customs: myCustoms }) +} ``` -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 +This adds a `GET` API route at `/customs`, which is the API route you added the middleware for. -The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options. +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. -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. +### Test it Out -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: +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. -```ts highlights={retrieveHighlights} -const product = await helloModuleService.retrieveProducts( - "123", - { - relations: ["orders"], - } -) +```json title="Returned Data" +{ + "my_customs": [ + { + "id": "123", + "name": "test" + } + ] +} ``` -In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. - +Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result. -# Data Model Relationships +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. -In this chapter, you’ll learn how to define relationships between data models in your module. -## What is a Relationship Property? +# Query Context -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`. +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). -When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type. +## What is Query Context? -You want to create a relation between data models in the same module. +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. -You want to create a relationship between data models in different modules. Use module links instead. +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). *** -## One-to-One Relationship - -A one-to-one relationship indicates that one record of a data model belongs to or is associated with another. - -To define a one-to-one relationship, create relationship properties in the data models using the following methods: - -1. `hasOne`: indicates that the model has one record of the specified model. -2. `belongsTo`: indicates that the model belongs to one record of the specified model. +## How to Use Query Context -For example: +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`). -```ts highlights={oneToOneHighlights} -import { model } from "@medusajs/framework/utils" +You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument. -const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email), -}) +For example, to retrieve posts using Query while passing the user's language as a context: -const Email = model.define("email", { - id: model.id().primaryKey(), - user: model.belongsTo(() => User, { - mappedBy: "email", +```ts +const { data } = await query.graph({ + entity: "post", + fields: ["*"], + context: QueryContext({ + lang: "es", }), }) ``` -In the example above, a user has one email, and an email belongs to one user. - -The `hasOne` and `belongsTo` methods accept a function as 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. +In this example, you pass in the context a `lang` property whose value is `es`. -### Optional Relationship +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. -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). +For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context: -### One-sided One-to-One Relationship +```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" -If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. +class BlogModuleService extends MedusaService({ + Post, + Author, +}){ + // @ts-ignore + async listPosts( + filters?: any, + config?: FindConfig<any> | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context -For example: + let posts = await super.listPosts(filters, config, sharedContext) -```ts highlights={oneToOneUndefinedHighlights} -import { model } from "@medusajs/framework/utils" + if (context.lang === "es") { + posts = posts.map((post) => { + return { + ...post, + title: post.title + " en español", + } + }) + } -const User = model.define("user", { - id: model.id().primaryKey(), -}) + return posts + } +} -const Email = model.define("email", { - id: model.id().primaryKey(), - user: model.belongsTo(() => User, { - mappedBy: undefined, - }), -}) +export default BlogModuleService ``` -### One-to-One Relationship in the Database - -When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property: - -1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column. -2. A foreign key on the `{relation_name}_id` column to the table of the related data model. - - - -*** +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. -## One-to-Many Relationship +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. -A one-to-many relationship indicates that one record of a data model has many records of another data model. +All posts returned will now have their titles appended with "en español". -To define a one-to-many relationship, create relationship properties in the data models using the following methods: +Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md). -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. +### 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={oneToManyHighlights} -import { model } from "@medusajs/framework/utils" +For example, following along with the previous example, you must override the `listAndCountPosts` method of the Blog Module's service: -const Store = model.define("store", { - id: model.id().primaryKey(), - products: model.hasMany(() => Product), -}) +```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" -const Product = model.define("product", { - id: model.id().primaryKey(), - store: model.belongsTo(() => Store, { - mappedBy: "products", - }), -}) -``` - -In this example, a store has many products, but a product belongs to one store. - -### Optional Relationship +class BlogModuleService extends MedusaService({ + Post, + Author, +}){ + // @ts-ignore + async listAndCountPosts( + filters?: any, + config?: FindConfig<any> | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context -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). + const result = await super.listAndCountPosts( + filters, + config, + sharedContext + ) -### One-to-Many Relationship in the Database + if (context.lang === "es") { + result.posts = posts.map((post) => { + return { + ...post, + title: post.title + " en español", + } + }) + } -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: + return result + } +} -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. +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`. *** -## Many-to-Many Relationship - -A many-to-many relationship indicates that many records of a data model can be associated with many records of another data model. - -To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method. +## Passing Query Context to Related Data Models -For example: +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. -```ts highlights={manyToManyHighlights} -import { model } from "@medusajs/framework/utils" +For linked data models, check out the [next section](#passing-query-context-to-linked-data-models). -const Order = model.define("order", { - id: model.id().primaryKey(), - products: model.manyToMany(() => Product, { - mappedBy: "orders", - pivotTable: "order_product", - joinColumn: "order_id", - inverseJoinColumn: "product_id", - }), -}) +For example, to pass a context for the post's authors: -const Product = model.define("product", { - id: model.id().primaryKey(), - orders: model.manyToMany(() => Order, { - mappedBy: "products", +```ts highlights={highlights3} +const { data } = await query.graph({ + entity: "post", + fields: ["*"], + context: QueryContext({ + lang: "es", + author: QueryContext({ + lang: "es", + }), }), }) ``` -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. +Then, in the `listPosts` method, you can handle the context for the post's authors: -### Many-to-Many Relationship in the Database +```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" -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`. +class BlogModuleService extends MedusaService({ + Post, + Author, +}){ + // @ts-ignore + async listPosts( + filters?: any, + config?: FindConfig<any> | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context -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. + let posts = await super.listPosts(filters, config, sharedContext) -The pivot table has columns with foreign keys pointing to the primary key of the associated tables. The column's name is either: + const isPostLangEs = context.lang === "es" + const isAuthorLangEs = context.author?.lang === "es" -- 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`. + 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, + }, + } + }) + } - + return posts + } +} -### Many-To-Many with Custom Columns +export default BlogModuleService +``` -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. +The context in `filters` will also have the context for `author`, which you can use to make transformations to the post's authors. -For example: +*** -```ts highlights={manyToManyColumnHighlights} -import { model } from "@medusajs/framework/utils" +## Passing Query Context to Linked Data Models -export const Order = model.define("order_test", { - id: model.id().primaryKey(), - products: model.manyToMany(() => Product, { - pivotEntity: () => OrderProduct, - }), -}) +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. -export const Product = model.define("product_test", { - id: model.id().primaryKey(), - orders: model.manyToMany(() => Order), -}) +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: -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(), +```ts highlights={highlights5} +const { data } = await query.graph({ + entity: "product", + fields: ["*", "post.*"], + context: { + post: QueryContext({ + lang: "es", + }), + }, }) ``` -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: +In this example, you retrieve products and their associated posts. You also pass a context for `post`, indicating the customer's language. -- `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. +To handle the context, you override the generated `listPosts` method of the Blog Module as explained [previously](#how-to-use-query-context). -*** -## Set Relationship Name in the Other Model +# Create a Plugin -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. +In this chapter, you'll learn how to create a Medusa plugin and publish it. -This is useful if the relationship property’s name is different from that of the associated data model. +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. -As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). -For example: +## 1. Create a Plugin Project -```ts highlights={relationNameHighlights} -import { model } from "@medusajs/framework/utils" +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. -const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email, { - mappedBy: "owner", - }), -}) +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: -const Email = model.define("email", { - id: model.id().primaryKey(), - owner: model.belongsTo(() => User, { - mappedBy: "email", - }), -}) +```bash +npx create-medusa-app my-plugin --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`. +This will create a new Medusa plugin project in the `my-plugin` directory. -*** +### Plugin Directory Structure -## Cascades +After the installation is done, the plugin structure will look like this: -When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it. + -For example, if a store is deleted, its products should also be deleted. +- `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. -The `cascades` method used on a data model configures which child records an operation is cascaded to. +*** -For example: +## 2. Prepare Plugin -```ts highlights={highlights} -import { model } from "@medusajs/framework/utils" +### Package Name -const Store = model.define("store", { - id: model.id().primaryKey(), - products: model.hasMany(() => Product), -}) -.cascades({ - delete: ["products"], -}) +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. -const Product = model.define("product", { - id: model.id().primaryKey(), - store: model.belongsTo(() => Store, { - mappedBy: "products", - }), -}) +For example: + +```json title="package.json" +{ + "name": "@myorg/plugin-name", + // ... +} ``` -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. +### Package Keywords -In the example above, when a store is deleted, its associated products are also deleted. +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" + ], + // ... +} +``` -# Write Migration +### Package Dependencies -In this chapter, you'll learn how to create a migration and write it manually. +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. -## What is a Migration? +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. -A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It has two methods: +For example, assuming `2.5.0` is the latest Medusa version: -- The `up` method reflects changes on the database. -- The `down` method reverts the changes made in the `up` method. +```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", + } +} +``` *** -## How to Write a Migration? - -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. +## 3. Publish Plugin Locally for Development and Testing -Alternatively, you can manually create a migration file under the `migrations` directory of your module. +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. -For example: +### Publish and Install Local Package -```ts title="src/modules/hello/migrations/Migration20240429.ts" -import { Migration } from "@mikro-orm/migrations" +### Prerequisites -export class Migration20240702105919 extends Migration { +- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) - async up(): Promise<void> { - this.addSql("create table if not exists \"my_custom\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"my_custom_pkey\" primary key (\"id\"));") - } +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. - async down(): Promise<void> { - this.addSql("drop table if exists \"my_custom\" cascade;") - } +To publish the plugin to the local registry, run the following command in your plugin project: -} +```bash title="Plugin project" +npx medusa plugin:publish ``` -The migration's file name should be of the format `Migration{YEAR}{MONTH}{DAY}.ts`. The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. - -In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax. +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`. -In the example above, the `up` method creates the table `my_custom`, and the `down` method drops the table if the migration is reverted. +Next, navigate to your Medusa application: -Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. +```bash title="Medusa application" +cd ~/path/to/medusa-app +``` -*** +Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. -## Run the Migration +Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: -To run your migration, run the following command: +```bash npm2yarn title="Medusa application" +npm install --save-dev yalc +``` -This command also syncs module links. If you don't want that, use the `--skip-links` option. +After that, run the following Medusa CLI command to install the plugin: -```bash -npx medusa db:migrate +```bash title="Medusa application" +npx medusa plugin:add @myorg/plugin-name ``` -This reflects the changes in the database as implemented in the migration's `up` method. +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. -*** +### Register Plugin in Medusa Application -## Rollback the Migration +After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. -To rollback or revert the last migration you ran for a module, run the following command: +Add the plugin to the `plugins` array in the `medusa-config.ts` file: -```bash -npx medusa db:rollback helloModuleService +```ts title="medusa-config.ts" highlights={pluginHighlights} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: {}, + }, + ], +}) ``` -This rolls back the last ran migration on the Hello Module. - -*** - -## More Database Commands +The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package. -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). +#### Pass Module Options through Plugin +Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. -# Searchable Data Model Property +For example: -In this chapter, you'll learn what a searchable property is and how to define it. +```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: { + apiKey: true, + }, + }, + ], +}) +``` -## What is a Searchable Property? +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). -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. +### Watch Plugin Changes During Development -When the `q` filter is passed, the data model's searchable properties are queried to find matching records. +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: -## Define a Searchable Property +```bash title="Plugin project" +npx medusa plugin:develop +``` -Use the `searchable` method on a `text` property to indicate that it's searchable. +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={searchableHighlights} -import { model } from "@medusajs/framework/utils" +### Start Medusa Application -const MyCustom = model.define("my_custom", { - name: model.text().searchable(), - // ... -}) +You can start your Medusa application's development server to test out your plugin: -export default MyCustom +```bash npm2yarn title="Medusa application" +npm run dev ``` -In this example, the `name` property is searchable. +While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. -### Search Example +*** -If you pass a `q` filter to the `listMyCustoms` method: +## 4. Create Customizations in the Plugin -```ts -const myCustoms = await helloModuleService.listMyCustoms({ - q: "John", -}) -``` +You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. -This retrieves records that include `John` in their `name` property. +- [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). -# Add Columns to a Link +### Generating Migrations for Modules -In this chapter, you'll learn how to add custom columns to a link definition and manage them. +During your development, you may need to generate migrations for modules in your plugin. To do that, use the `plugin:db:generate` command: -## How to Add Custom Columns to a Link's Table? +```bash title="Plugin project" +npx medusa plugin:db:generate +``` -The `defineLink` function used to define a link accepts a third parameter, which is an object of options. +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: -To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property: +```bash title="Medusa application" +npx medusa db:migrate +``` -```ts highlights={linkHighlights} -import HelloModule from "../modules/hello" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" +### Importing Module Resources -export default defineLink( - ProductModule.linkable.product, - HelloModule.linkable.myCustom, - { - database: { - extraColumns: { - metadata: { - type: "json", - }, - }, - }, +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" } -) +} ``` -This adds to the table created for the link between `product` and `myCustom` a `metadata` column of type `json`. - -### Database Options +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 `database` property defines configuration for the table created in the database. +The plugin exports the following files and directories: -Its `extraColumns` property defines custom columns to create in the link's table. +- `./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. -`extraColumns`'s value is an object whose keys are the names of the columns, and values are the column's configurations as an object. +With these exports, you can import the plugin's resources in the Medusa application's code like this: -### Column Configurations +`@myorg/plugin-name` is the plugin package's name. -The column's configurations object accepts the following properties: +```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" +``` -- `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. +And you can register a module provider in the Medusa application's `medusa-config.ts` like this: -*** +```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... + }, + }, + ], + }, + }, + ], +}) +``` -## Set Custom Column when Creating Link +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. -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. +### Create Module Providers -For example: +To learn how to create module providers, refer to the following guides: -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). +- [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 -await link.create({ - [Modules.PRODUCT]: { - product_id: "123", - }, - HELLO_MODULE: { - my_custom_id: "321", - }, - data: { - metadata: { - test: true, - }, - }, -}) +*** + +## 5. Publish Plugin to NPM + +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: + +```bash +npx medusa plugin:build ``` -*** +The command will compile an output in the `.medusa/server` directory. -## Retrieve Custom Column with Link +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: -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. +```bash +npm publish +``` -For example: +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. -```ts highlights={retrieveHighlights} -import productHelloLink from "../links/product-hello" +### Install Public Plugin in Medusa Application -// ... +You install a plugin that's published publicly using your package manager. For example: -const { data } = await query.graph({ - entity: productHelloLink.entryPoint, - fields: ["metadata", "product.*", "my_custom.*"], - filters: { - product_id: "prod_123", - }, -}) +```bash npm2yarn +npm install @myorg/plugin-name ``` -This retrieves the product of id `prod_123` and its linked `my_custom` records. +Where `@myorg/plugin-name` is the name of your plugin as published on NPM. -In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link. +Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). *** -## Update Custom Column's Value +## Update a Published Plugin -Link's `create` method updates a link's data if the link between the specified records already exists. +To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md). -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. +If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. -For example: +First, run the following command to change the version of the plugin: -```ts -await link.create({ - [Modules.PRODUCT]: { - product_id: "123", - }, - HELLO_MODULE: { - my_custom_id: "321", - }, - data: { - metadata: { - test: false, - }, - }, -}) +```bash +npm version <type> ``` +Where `<type>` 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. -# Query +Then, re-run the same commands for publishing a plugin: -In this chapter, you’ll learn about Query and how to use it to fetch data from modules. +```bash +npx medusa plugin:build +npm publish +``` -## What is Query? +This will publish an updated version of your plugin under a new version. -Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key. -In your resources, such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s commerce modules. +# Add Data Model Check Constraints -*** +In this chapter, you'll learn how to add check constraints to your data model. -## Query Example +## What is a Check Constraint? -For example, create the route `src/api/query/route.ts` with the following content: +A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown. -```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" +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. -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) +*** - const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], - }) +## How to Set a Check Constraint? - res.json({ my_customs: myCustoms }) -} -``` +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. -In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key. +For example, to set a check constraint on a `price` property that ensures its value can only be a positive number: -Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties: +```ts highlights={checks1Highlights} +import { model } from "@medusajs/framework/utils" -- `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. +const CustomProduct = model.define("custom_product", { + // ... + price: model.bigNumber(), +}) +.checks([ + (columns) => `${columns.price} >= 0`, +]) +``` -The method returns an object that has a `data` property, which holds an array of the retrieved data. For example: +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. -```json title="Returned Data" -{ - "data": [ - { - "id": "123", - "name": "test" - } - ] -} -``` +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: -## Querying the Graph +```ts highlights={checks2Highlights} +import { model } from "@medusajs/framework/utils" -When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates. +const CustomProduct = model.define("custom_product", { + // ... + price: model.bigNumber(), +}) +.checks([ + { + name: "custom_product_price_check", + expression: (columns) => `${columns.price} >= 0`, + }, +]) +``` -This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them. +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). *** -## Retrieve Linked Records +## Apply in Migrations -Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`. +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. -For example: +To generate a migration for the data model's module then reflect it on the database, run the following command: -```ts highlights={[["6"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: [ - "id", - "name", - "product.*", - ], -}) +```bash +npx medusa db:generate custom_module +npx medusa db:migrate ``` -`.*` 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 first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. -### Retrieve List Link Records -If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`. +# Configure Data Model Properties + +In this chapter, you’ll learn how to configure data model properties. + +## Property’s Default Value + +Use the `default` method on a property's definition to specify the default value of a property. For example: -```ts highlights={[["6"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: [ - "id", - "name", - "products.*", - ], +```ts highlights={defaultHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + color: model + .enum(["black", "white"]) + .default("black"), + age: model + .number() + .default(0), + // ... }) + +export default MyCustom ``` -### Apply Filters and Pagination on Linked Records +In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`. -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. +*** -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. +## Nullable Property -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. +Use the `nullable` method to indicate that a property’s value can be `null`. For example: -```ts highlights={queryLinkTableHighlights} -import productCustomLink from "../../../links/product-custom" - -// ... +```ts highlights={nullableHighlights} +import { model } from "@medusajs/framework/utils" -const { data: productCustoms } = await query.graph({ - entity: productCustomLink.entryPoint, - fields: ["*", "product.*", "my_custom.*"], - pagination: { - take: 5, - skip: 0, - }, +const MyCustom = model.define("my_custom", { + price: model.bigNumber().nullable(), + // ... }) + +export default MyCustom ``` -In the object passed to the `graph` method: +*** -- 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. +## Unique Property -You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination). +The `unique` method indicates that a property’s value must be unique in the database through a unique index. -The returned `data` is similar to the following: +For example: -```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... - } -}] +```ts highlights={uniqueHighlights} +import { model } from "@medusajs/framework/utils" + +const User = model.define("user", { + email: model.text().unique(), + // ... +}) + +export default User ``` -*** +In this example, multiple users can’t have the same email. -## Apply Filters -```ts highlights={[["6"], ["7"], ["8"], ["9"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], - filters: { - id: [ - "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX", - "mc_01HWSVWK3KYHKQEE6QGS2JC3FX", - ], - }, -}) -``` +# Infer Type of Data Model -The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records. +In this chapter, you'll learn how to infer the type of a data model. -In the example above, you filter the `my_custom` records by multiple IDs. +## How to Infer Type of Data Model? -Filters don't apply on fields of linked data models from other modules. +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. -## Apply Pagination +For example: -```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, - }, -}) -``` +```ts +import { InferTypeOf } from "@medusajs/framework/types" +import { MyCustom } from "../models/my-custom" // relative path to the model -The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records. +export type MyCustom = InferTypeOf<typeof MyCustom> +``` -To paginate the returned records, pass the following properties to `pagination`: +`InferTypeOf` accepts as a type argument the type of the data model. -- `skip`: (required to apply pagination) The number of records to skip before fetching the results. -- `take`: The number of records to fetch. +Since the `MyCustom` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. -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: +You can now use the `MyCustom` type to reference a data model in other types, such as in workflow inputs or service method outputs: -- skip: (\`number\`) The number of records skipped. -- take: (\`number\`) The number of records requested to fetch. -- count: (\`number\`) The total number of records. +```ts title="Example Service" +// other imports... +import { InferTypeOf } from "@medusajs/framework/types" +import { MyCustom } from "../models/my-custom" -### Sort Records +type MyCustom = InferTypeOf<typeof MyCustom> -```ts highlights={[["5"], ["6"], ["7"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], - pagination: { - order: { - name: "DESC", - }, - }, -}) +class HelloModuleService extends MedusaService({ MyCustom }) { + async doSomething(): Promise<MyCustom> { + // ... + } +} ``` -Sorting doesn't work on fields of linked data models from other modules. - -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: +# Data Model Default 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 about the properties available by default in your data model. -*** +When you create a data model, the following properties are created for you by Medusa: -## Request Query Configurations +- `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. -For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that: -- 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. +# Data Model Database Index -Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request. +In this chapter, you’ll learn how to define a database index on a data model. -### Step 1: Add Middleware +## Define Database Index on Property -The first step is to use the `validateAndTransformQuery` middleware on the `GET` route. You add the middleware in `src/api/middlewares.ts`: +Use the `index` method on a property's definition to define a database index. -```ts title="src/api/middlewares.ts" -import { - validateAndTransformQuery, - defineMiddlewares, -} from "@medusajs/framework/http" -import { createFindParams } from "@medusajs/medusa/api/utils/validators" +For example: -export const GetCustomSchema = createFindParams() +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" -export default defineMiddlewares({ - routes: [ - { - matcher: "/customs", - method: "GET", - middlewares: [ - validateAndTransformQuery( - GetCustomSchema, - { - defaults: [ - "id", - "name", - "products.*", - ], - isList: true, - } - ), - ], - }, - ], +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text().index( + "IDX_MY_CUSTOM_NAME" + ), }) + +export default MyCustom ``` -The `validateAndTransformQuery` accepts two parameters: +The `index` method optionally accepts the name of the index as a parameter. -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`. +In this example, you define an index on the `name` property. -### Step 2: Use Configurations in API Route +*** -After applying this middleware, your API route now accepts the `fields`, `offset`, `limit`, and `order` query parameters mentioned above. +## Define Database Index on Data Model -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. +A data model has an `indexes` method that defines database indices on its properties. -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. +The index can be on multiple columns (composite index). For example: -For example, Create the file `src/api/customs/route.ts` with the following content: +```ts highlights={dataModelIndexHighlights} +import { model } from "@medusajs/framework/utils" -```ts title="src/api/customs/route.ts" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - ContainerRegistrationKeys, -} 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 const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) +export default MyCustom +``` - const { data: myCustoms } = await query.graph({ - entity: "my_custom", - ...req.queryConfig, - }) +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. - res.json({ my_customs: myCustoms }) -} -``` +In the above example, you define a composite index on the `name` and `age` properties. -This adds a `GET` API route at `/customs`, which is the API route you added the middleware for. +### Index Conditions -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. +An index can have conditions. For example: -### Test it Out +```ts highlights={conditionHighlights} +import { model } from "@medusajs/framework/utils" -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. +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + where: { + age: 30, + }, + }, +]) -```json title="Returned Data" -{ - "my_customs": [ - { - "id": "123", - "name": "test" - } - ] -} +export default MyCustom ``` -Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result. +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. -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. +In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`. +A property's condition can be a negation. For example: -# Link +```ts highlights={negationHighlights} +import { model } from "@medusajs/framework/utils" -In this chapter, you’ll learn what Link is and how to use it to manage links. +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, + }, + }, + }, +]) -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. +export default MyCustom +``` -## What is Link? +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. -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. +In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`. -For example: +### Unique Database Index -```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index. -export async function POST( - req: MedusaRequest, - res: MedusaResponse -): Promise<void> { - const link = req.scope.resolve( - ContainerRegistrationKeys.LINK - ) - - // ... -} -``` +For example: -You can use its methods to manage links, such as create or delete links. +```ts highlights={uniqueHighlights} +import { model } from "@medusajs/framework/utils" -*** +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + unique: true, + }, +]) -## Create Link +export default MyCustom +``` -To create a link between records of two data models, use the `create` method of Link. +This creates a unique composite index on the `name` and `age` properties. -For example: -```ts -import { Modules } from "@medusajs/framework/utils" +# Manage Relationships -// ... +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. -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_123", - }, -}) -``` +## Manage One-to-One Relationship -The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules. +### BelongsTo Side of One-to-One -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. +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. -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. +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: -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. +```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", +}) +``` -## Dismiss Link +In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to. -To remove a link between records of two data models, use the `dismiss` method of Link. +### 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 -import { Modules } 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: -// ... +```ts highlights={hasOneHighlights} +// when creating a user +const user = await helloModuleService.createUsers({ + // other properties... + email: "123", +}) -await link.dismiss({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_123", - }, +// when updating a user +const user = await helloModuleService.updateUsers({ + id: "321", + // other properties... + email: "123", }) ``` -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. +In the example above, you pass the `email` property when creating or updating a user to specify the email it has. *** -## Cascade Delete Linked Records - -If a record is deleted, use the `delete` method of Link to delete all linked records. +## Manage One-to-Many Relationship -For example: +In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. -```ts -import { Modules } from "@medusajs/framework/utils" +When you create a record of the data model on the `belongsTo` side, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property. -// ... +For example, assuming you have the [Product and Store data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-many-relationship/index.html.md), set a product's store ID as follows: -await productModuleService.deleteVariants([variant.id]) +```ts highlights={manyBelongsHighlights} +// when creating a product +const product = await helloModuleService.createProducts({ + // other properties... + store_id: "123", +}) -await link.delete({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, +// when updating a product +const product = await helloModuleService.updateProducts({ + id: "321", + // other properties... + store_id: "123", }) ``` -This deletes all records linked to the deleted product. +In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to. *** -## Restore Linked Records +## Manage Many-to-Many Relationship -If a record that was previously soft-deleted is now restored, use the `restore` method of Link to restore all linked records. +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. -For example: +### Create Associations -```ts -import { Modules } from "@medusajs/framework/utils" +When you create a record of a data model that has a many-to-many relationship to another data model, pass an array of IDs of the other data model's records in the relation property. -// ... +For example, assuming you have the [Order and Product data models from the previous chapter](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: -await productModuleService.restoreProducts(["prod_123"]) +```ts highlights={manyHighlights} +// when creating a product +const product = await helloModuleService.createProducts({ + // other properties... + orders: ["123", "321"], +}) -await link.restore({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, +// when creating an order +const order = await helloModuleService.createOrders({ + id: "321", + // other properties... + products: ["123", "321"], }) ``` +In the example above, you pass the `orders` property when you create a product, and you pass the `products` property when you create an order. -# Module Link Direction - -In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case. +### Update Associations -## Link Direction +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. -The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`. +However, this removes any existing associations to records whose IDs aren't included in the array. -For example, the following defines a link from the `helloModuleService`'s `myCustom` data model to the Product Module's `product` data model: +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 -export default defineLink( - HelloModule.linkable.myCustom, - ProductModule.linkable.product -) +const product = await helloModuleService.updateProducts({ + id: "123", + // other properties... + orders: ["321"], +}) ``` -Whereas the following defines a link from the Product Module's `product` data model to the `helloModuleService`'s `myCustom` data model: +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. -```ts -export default defineLink( - ProductModule.linkable.product, - HelloModule.linkable.myCustom +So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product: + +```ts highlights={updateAssociationHighlights} +const product = await helloModuleService.retrieveProduct( + "123", + { + relations: ["orders"], + } ) + +const updatedProduct = await helloModuleService.updateProducts({ + id: product.id, + // other properties... + orders: [ + ...product.orders.map((order) => order.id), + "321", + ], +}) ``` -The above links are two different links that serve different purposes. +This keeps existing associations between the product and orders, and adds a new one. *** -## Which Link Direction to Use? +## Manage Many-to-Many Relationship with pivotEntity -### Extend Data Models +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. -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. +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. -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: +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: -```ts -export default defineLink( - ProductModule.linkable.product, - HelloModule.linkable.subtitle -) +```ts highlights={["4"]} +class HelloModuleService extends MedusaService({ + Order, + Product, + OrderProduct, +}) {} ``` -### Associate Data Models - -If you're linking data models to indicate an association between them, define the link from the custom data model to the main data model. +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, 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`: +For example: ```ts -export default defineLink( - HelloModule.linkable.post, - ProductModule.linkable.product -) -``` - - -# Query Context +// create order-product association +const orderProduct = await helloModuleService.createOrderProducts({ + order_id: "123", + product_id: "123", + metadata: { + test: true, + }, +}) -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). +// update order-product association +const orderProduct = await helloModuleService.updateOrderProducts({ + id: "123", + metadata: { + test: false, + }, +}) -## What is Query Context? +// delete order-product association +await helloModuleService.deleteOrderProducts("123") +``` -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. +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`. -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). +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. *** -## How to Use Query Context +## Retrieve Records of Relation -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`). +The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options. -You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument. +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, to retrieve posts using Query while passing the user's language as a context: +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 -const { data } = await query.graph({ - entity: "post", - fields: ["*"], - context: QueryContext({ - lang: "es", - }), -}) +```ts highlights={retrieveHighlights} +const product = await helloModuleService.retrieveProducts( + "123", + { + relations: ["orders"], + } +) ``` -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. - -For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context: +In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. -```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 BlogModuleService extends MedusaService({ - Post, - Author, -}){ - // @ts-ignore - async listPosts( - filters?: any, - config?: FindConfig<any> | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context +# Data Model’s Primary Key - let posts = await super.listPosts(filters, config, sharedContext) +In this chapter, you’ll learn how to configure the primary key of a data model. - if (context.lang === "es") { - posts = posts.map((post) => { - return { - ...post, - title: post.title + " en español", - } - }) - } +## primaryKey Method - return posts - } -} +To set any `id`, `text`, or `number` property as a primary key, use the `primaryKey` method. -export default BlogModuleService -``` +For example: -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. +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" -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. +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + // ... +}) -All posts returned will now have their titles appended with "en español". +export default MyCustom +``` -Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md). +In the example above, the `id` property is defined as the data model's primary key. -### Using Pagination with Query -If you pass pagination fields to `query.graph`, you must also override the `listAndCount` method in the service. +# Data Model Property Types -For example, following along with the previous example, you must override the `listAndCountPosts` method of the Blog Module's service: +In this chapter, you’ll learn about the types of properties in a data model’s schema. -```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" +## id -class BlogModuleService extends MedusaService({ - Post, - Author, -}){ - // @ts-ignore - async listAndCountPosts( - filters?: any, - config?: FindConfig<any> | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context +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. - const result = await super.listAndCountPosts( - filters, - config, - sharedContext - ) +For example: - if (context.lang === "es") { - result.posts = posts.map((post) => { - return { - ...post, - title: post.title + " en español", - } - }) - } +```ts highlights={idHighlights} +import { model } from "@medusajs/framework/utils" - return result - } -} +const MyCustom = model.define("my_custom", { + id: model.id(), + // ... +}) -export default BlogModuleService +export default MyCustom ``` -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 +## text -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. +The `text` method defines a string property. -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 highlights={textHighlights} +import { model } from "@medusajs/framework/utils" -```ts highlights={highlights3} -const { data } = await query.graph({ - entity: "post", - fields: ["*"], - context: QueryContext({ - lang: "es", - author: QueryContext({ - lang: "es", - }), - }), +const MyCustom = model.define("my_custom", { + name: model.text(), + // ... }) + +export default MyCustom ``` -Then, in the `listPosts` method, you can handle the context for the post's authors: +*** -```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" - -class BlogModuleService extends MedusaService({ - Post, - Author, -}){ - // @ts-ignore - async listPosts( - filters?: any, - config?: FindConfig<any> | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context +## number - let posts = await super.listPosts(filters, config, sharedContext) +The `number` method defines a number property. - 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={numberHighlights} +import { model } from "@medusajs/framework/utils" - return posts - } -} +const MyCustom = model.define("my_custom", { + age: model.number(), + // ... +}) -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. - *** -## Passing Query Context to Linked Data Models - -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. - -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: - -```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. - -To handle the context, you override the generated `listPosts` method of the Blog Module as explained [previously](#how-to-use-query-context). - - -# Create a Plugin +## float -In this chapter, you'll learn how to create a Medusa plugin and publish it. +This property is only available after [Medusa v2.1.2](https://github.com/medusajs/medusa/releases/tag/v2.1.2). -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 `float` method defines a number property that allows for values with decimal places. -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). +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). -## 1. Create a Plugin Project +For example: -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 highlights={floatHighlights} +import { model } from "@medusajs/framework/utils" -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: +const MyCustom = model.define("my_custom", { + rating: model.float(), + // ... +}) -```bash -npx create-medusa-app my-plugin --plugin +export default MyCustom ``` -This will create a new Medusa plugin project in the `my-plugin` directory. - -### Plugin Directory Structure - -After the installation is done, the plugin structure will look like this: - - - -- `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. - *** -## 2. Prepare Plugin +## bigNumber -### Package Name +The `bigNumber` method defines a number property that expects large numbers, such as prices. -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. +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). For example: -```json title="package.json" -{ - "name": "@myorg/plugin-name", +```ts highlights={bigNumberHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + price: model.bigNumber(), // ... -} -``` +}) -### Package Keywords +export default MyCustom +``` -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" - ], - // ... -} -``` +## boolean -### Package Dependencies +The `boolean` method defines a boolean property. -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: -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 highlights={booleanHighlights} +import { model } from "@medusajs/framework/utils" -For example, assuming `2.5.0` is the latest Medusa version: +const MyCustom = model.define("my_custom", { + hasAccount: model.boolean(), + // ... +}) -```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 MyCustom ``` *** -## 3. Publish Plugin Locally for Development and Testing - -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. - -### Publish and Install Local Package +### enum -### Prerequisites +The `enum` method defines a property whose value can only be one of the specified values. -- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) +For example: -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. +```ts highlights={enumHighlights} +import { model } from "@medusajs/framework/utils" -To publish the plugin to the local registry, run the following command in your plugin project: +const MyCustom = model.define("my_custom", { + color: model.enum(["black", "white"]), + // ... +}) -```bash title="Plugin project" -npx medusa plugin:publish +export default MyCustom ``` -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`. +The `enum` method accepts an array of possible string values. -Next, navigate to your Medusa application: +*** -```bash title="Medusa application" -cd ~/path/to/medusa-app -``` +## dateTime -Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. +The `dateTime` method defines a timestamp property. -Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: +For example: -```bash npm2yarn title="Medusa application" -npm install --save-dev yalc -``` +```ts highlights={dateTimeHighlights} +import { model } from "@medusajs/framework/utils" -After that, run the following Medusa CLI command to install the plugin: +const MyCustom = model.define("my_custom", { + date_of_birth: model.dateTime(), + // ... +}) -```bash title="Medusa application" -npx medusa plugin:add @myorg/plugin-name +export default MyCustom ``` -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. +*** -### Register Plugin in Medusa Application +## json -After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. +The `json` method defines a property whose value is a stringified JSON object. -Add the plugin to the `plugins` array in the `medusa-config.ts` file: +For example: -```ts title="medusa-config.ts" highlights={pluginHighlights} -module.exports = defineConfig({ +```ts highlights={jsonHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + metadata: model.json(), // ... - plugins: [ - { - resolve: "@myorg/plugin-name", - options: {}, - }, - ], }) + +export default MyCustom ``` -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 +## array -Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. +The `array` method defines an array of strings property. For example: -```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} -module.exports = defineConfig({ +```ts highlights={arrHightlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + names: model.array(), // ... - plugins: [ - { - resolve: "@myorg/plugin-name", - options: { - apiKey: true, - }, - }, - ], }) + +export default MyCustom ``` -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). +*** -### Watch Plugin Changes During Development +## Properties Reference -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. +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. -To do that, run the following command in your plugin project: -```bash title="Plugin project" -npx medusa plugin:develop -``` +# Data Model Relationships -This command will: +In this chapter, you’ll learn how to define relationships between data models in your module. -- 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. +## What is a Relationship Property? -### Start Medusa Application +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`. -You can start your Medusa application's development server to test out your plugin: +When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type. -```bash npm2yarn title="Medusa application" -npm run dev -``` +You want to create a relation between data models in the same module. -While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. +You want to create a relationship between data models in different modules. Use module links instead. *** -## 4. Create Customizations in the Plugin - -You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. +## One-to-One Relationship -- [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) +A one-to-one relationship indicates that one record of a data model belongs to or is associated with another. -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). +To define a one-to-one relationship, create relationship properties in the data models using the following methods: -### Generating Migrations for Modules +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. -During your development, you may need to generate migrations for modules in your plugin. To do that, use the `plugin:db:generate` command: +For example: -```bash title="Plugin project" -npx medusa plugin:db:generate -``` +```ts highlights={oneToOneHighlights} +import { model } from "@medusajs/framework/utils" -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: +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email), +}) -```bash title="Medusa application" -npx medusa db:migrate +const Email = model.define("email", { + id: model.id().primaryKey(), + user: model.belongsTo(() => User, { + mappedBy: "email", + }), +}) ``` -### Importing Module Resources +In the example above, a user has one email, and an email belongs to one user. -Your plugin project should have the following exports in `package.json`: +The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model. -```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" - } -} -``` +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. -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. +### Optional Relationship -The plugin exports the following files and directories: +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). -- `./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. +### One-sided One-to-One Relationship -With these exports, you can import the plugin's resources in the Medusa application's code like this: +If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. -`@myorg/plugin-name` is the plugin package's name. +For example: -```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" -``` +```ts highlights={oneToOneUndefinedHighlights} +import { model } from "@medusajs/framework/utils" -And you can register a module provider in the Medusa application's `medusa-config.ts` like this: +const User = model.define("user", { + id: model.id().primaryKey(), +}) -```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... - }, - }, - ], - }, - }, - ], +const Email = model.define("email", { + id: model.id().primaryKey(), + user: model.belongsTo(() => User, { + mappedBy: undefined, + }), }) ``` -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. +### One-to-One Relationship in the Database -### Create Module Providers +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: -To learn how to create module providers, refer to the following guides: +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. -- [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) + *** -## 5. Publish Plugin to NPM +## One-to-Many Relationship -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: +A one-to-many relationship indicates that one record of a data model has many records of another data model. -```bash -npx medusa plugin:build -``` +To define a one-to-many relationship, create relationship properties in the data models using the following methods: -The command will compile an output in the `.medusa/server` directory. +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. -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: +For example: -```bash -npm publish +```ts highlights={oneToManyHighlights} +import { model } from "@medusajs/framework/utils" + +const Store = model.define("store", { + id: model.id().primaryKey(), + products: model.hasMany(() => Product), +}) + +const Product = model.define("product", { + id: model.id().primaryKey(), + store: model.belongsTo(() => Store, { + mappedBy: "products", + }), +}) ``` -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. +In this example, a store has many products, but a product belongs to one store. -### Install Public Plugin in Medusa Application +### Optional Relationship -You install a plugin that's published publicly using your package manager. For example: +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). -```bash npm2yarn -npm install @myorg/plugin-name -``` +### One-to-Many Relationship in the Database -Where `@myorg/plugin-name` is the name of your plugin as published on NPM. +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: -Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). +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. -*** + -## Update a Published Plugin +*** -To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md). +## Many-to-Many Relationship -If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. +A many-to-many relationship indicates that many records of a data model can be associated with many records of another data model. -First, run the following command to change the version of the plugin: +To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method. -```bash -npm version <type> -``` +For example: -Where `<type>` 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 highlights={manyToManyHighlights} +import { model } from "@medusajs/framework/utils" -Then, re-run the same commands for publishing a plugin: +const Order = model.define("order", { + id: model.id().primaryKey(), + products: model.manyToMany(() => Product, { + mappedBy: "orders", + pivotTable: "order_product", + joinColumn: "order_id", + inverseJoinColumn: "product_id", + }), +}) -```bash -npx medusa plugin:build -npm publish +const Product = model.define("product", { + id: model.id().primaryKey(), + orders: model.manyToMany(() => Order, { + mappedBy: "products", + }), +}) ``` -This will publish an updated version of your plugin under a new version. +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. -# Commerce Modules +The `pivotTable`, `joinColumn`, and `inverseJoinColumn` properties are only available after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). -In this chapter, you'll learn about Medusa's commerce modules. +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. -## What is a Commerce Module? +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 + +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`. + +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. + +The pivot table has columns with foreign keys pointing to the primary key of the associated tables. The column's name is either: + +- 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`. + + + +### Many-To-Many with Custom Columns + +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: + +```ts highlights={manyToManyColumnHighlights} +import { model } from "@medusajs/framework/utils" + +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), +}) + +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(), +}) +``` + +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: + +- `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. + +*** + +## 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. + +As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. + +For example: + +```ts highlights={relationNameHighlights} +import { model } from "@medusajs/framework/utils" + +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email, { + mappedBy: "owner", + }), +}) + +const Email = model.define("email", { + id: model.id().primaryKey(), + owner: model.belongsTo(() => User, { + mappedBy: "email", + }), +}) +``` + +In this example, you specify in the `User` data model’s relationship property that the name of the relationship in the `Email` data model is `owner`. + +*** + +## Cascades + +When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it. + +For example, if a store is deleted, its products should also be deleted. + +The `cascades` method used on a data model configures which child records an operation is cascaded to. + +For example: + +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" + +const Store = model.define("store", { + id: model.id().primaryKey(), + products: model.hasMany(() => Product), +}) +.cascades({ + delete: ["products"], +}) + +const Product = model.define("product", { + id: model.id().primaryKey(), + store: model.belongsTo(() => Store, { + mappedBy: "products", + }), +}) +``` + +The `cascades` method accepts an object. Its key is the operation’s name, such as `delete`. The value is an array of relationship property names that the operation is cascaded to. + +In the example above, when a store is deleted, its associated products are also deleted. + + +# Searchable Data Model Property + +In this chapter, you'll learn what a searchable property is and how to define it. + +## What is a Searchable Property? + +Methods generated by the [service factory](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. + +*** + +## Define a Searchable Property + +Use the `searchable` method on a `text` property to indicate that it's searchable. + +For example: + +```ts highlights={searchableHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + name: model.text().searchable(), + // ... +}) + +export default MyCustom +``` + +In this example, the `name` property is searchable. + +### Search Example + +If you pass a `q` filter to the `listMyCustoms` method: + +```ts +const myCustoms = await helloModuleService.listMyCustoms({ + q: "John", +}) +``` + +This retrieves records that include `John` in their `name` property. + + +# Write Migration + +In this chapter, you'll learn how to create a migration and write it manually. + +## What is a Migration? + +A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It has two methods: + +- The `up` method reflects changes on the database. +- The `down` method reverts the changes made in the `up` method. + +*** + +## How to Write a Migration? + +The Medusa CLI tool provides a [db:generate](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbgenerate/index.html.md) command to generate a migration for the specified modules' data models. + +Alternatively, you can manually create a migration file under the `migrations` directory of your module. + +For example: + +```ts title="src/modules/hello/migrations/Migration20240429.ts" +import { Migration } from "@mikro-orm/migrations" + +export class Migration20240702105919 extends Migration { + + async up(): Promise<void> { + this.addSql("create table if not exists \"my_custom\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"my_custom_pkey\" primary key (\"id\"));") + } + + async down(): Promise<void> { + this.addSql("drop table if exists \"my_custom\" cascade;") + } + +} +``` + +The migration's file name should be of the format `Migration{YEAR}{MONTH}{DAY}.ts`. The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. + +In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax. + +In the example above, the `up` method creates the table `my_custom`, and the `down` method drops the table if the migration is reverted. + +Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. + +*** + +## Run the Migration + +To run your migration, run the following command: + +This command also syncs module links. If you don't want that, use the `--skip-links` option. + +```bash +npx medusa db:migrate +``` + +This reflects the changes in the database as implemented in the migration's `up` method. + +*** + +## Rollback the Migration + +To rollback or revert the last migration you ran for a module, run the following command: + +```bash +npx medusa db:rollback helloModuleService +``` + +This rolls back the last ran migration on the Hello Module. + +*** + +## More Database Commands + +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). + + +# Commerce Modules + +In this chapter, you'll learn about Medusa's commerce modules. + +## What is a Commerce Module? 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. @@ -10189,225 +10529,569 @@ export default async function helloWorldLoader({ ``` -# Perform Database Operations in a Service +# Module Isolation -In this chapter, you'll learn how to perform database operations in a module's service. +In this chapter, you'll learn how modules are isolated, and what that means for your custom development. -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. +- 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. -## Run Queries +## How are Modules Isolated? -[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. +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. -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, 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. -So, to run database queries in a service: +*** -1. Add the `InjectManager` decorator to the method. -2. Add as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator from the Modules SDK. This context holds database-related context, including the manager injected by `InjectManager` +## Why are Modules Isolated -For example, in your service, add the following methods: +Some of the module isolation's benefits include: -```ts highlights={methodsHighlight} -// other imports... -import { - InjectManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { SqlEntityManager } from "@mikro-orm/knex" +- 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. -class HelloModuleService { - // ... +*** - @InjectManager() - async getCount( - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<number> { - return await sharedContext.manager.count("my_custom") - } - - @InjectManager() - async getCountSql( - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<number> { - const data = await sharedContext.manager.execute( - "SELECT COUNT(*) as num FROM my_custom" - ) - - return parseInt(data[0].num) - } -} -``` +## How to Extend Data Model of Another Module? -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. +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 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). +*** -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. +## How to Use Services of Other Modules? -Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. +If you're building a feature that uses functionalities from different modules, use a workflow whose steps resolve the modules' services to perform these functionalities. -*** +Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. -## Execute Operations in Transactions +### Example -To wrap database operations in a transaction, you create two methods: +For example, consider you have two modules: -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. +1. A module that stores and manages brands in your application. +2. A module that integrates a third-party Content Management System (CMS). -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. +To sync brands from your application to the third-party system, create the following steps: -For example: +```ts title="Example Steps" highlights={stepsHighlights} +const retrieveBrandsStep = createStep( + "retrieve-brands", + async (_, { container }) => { + const brandModuleService = container.resolve( + "brandModuleService" + ) -```ts highlights={opHighlights} -import { - InjectManager, - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" + const brands = await brandModuleService.listBrands() -class HelloModuleService { - // ... - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<any> { - const transactionManager = sharedContext.transactionManager - await transactionManager.nativeUpdate( - "my_custom", - { - id: input.id, - }, - { - name: input.name, - } + return new StepResponse(brands) + } +) + +const createBrandsInCmsStep = createStep( + "create-brands-in-cms", + async ({ brands }, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" ) - // retrieve again - const updatedRecord = await transactionManager.execute( - `SELECT * FROM my_custom WHERE id = '${input.id}'` + const cmsBrands = await cmsModuleService.createBrands(brands) + + return new StepResponse(cmsBrands, cmsBrands) + }, + async (brands, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" ) - return updatedRecord + await cmsModuleService.deleteBrands( + brands.map((brand) => brand.id) + ) } +) +``` - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ) { - return await this.update_(input, sharedContext) +The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module. + +Then, create the following workflow that uses these steps: + +```ts title="Example Workflow" +export const syncBrandsWorkflow = createWorkflow( + "sync-brands", + () => { + const brands = retrieveBrandsStep() + + createBrandsInCmsStep({ brands }) } -} +) ``` -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. +You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. -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. +# Loaders -### Why Wrap a Transactional Method +In this chapter, you’ll learn about loaders and how to use them. -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. +## What is a Loader? -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. +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. -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. +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. -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. +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, the `update` method could be changed to the following: +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 -// other imports... -import { EntityManager } from "@mikro-orm/knex" +*** -class HelloModuleService { - // ... - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ) { - const newData = await this.update_(input, sharedContext) +## How to Create a Loader? - await sendNewDataToSystem(newData) +### 1. Implement Loader Function - return newData - } -} -``` +You create a loader function in a TypeScript or JavaScript file under a module's `loaders` directory. -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. +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: -### Using Methods in Transactional Methods + -If your transactional method uses other methods that accept a Medusa context, pass the shared context to those methods. +Learn how to create a module in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -For example: +```ts title="src/modules/hello/loaders/hello-world.ts" +import { + LoaderOptions, +} from "@medusajs/framework/types" -```ts -// other imports... -import { EntityManager } from "@mikro-orm/knex" +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const logger = container.resolve("logger") -class HelloModuleService { - // ... - @InjectTransactionManager() - protected async anotherMethod( - @MedusaContext() sharedContext?: Context<EntityManager> - ) { - // ... - } - - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<any> { - this.anotherMethod(sharedContext) - } + logger.info("[helloWorldLoader]: Hello, World!") } ``` -You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context. +The loader file exports an async function, which is the function executed when the application loads. -The `anotherMethod` now runs in the same transaction as the `update_` method. +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. -*** +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). -## Configure Transactions +### 2. Export Loader in Module Definition -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. +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. -The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more. +So, to export the loader you implemented above in the `hello` module, add the following to `src/modules/hello/index.ts`: -The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction. +```ts title="src/modules/hello/index.ts" +// other imports... +import helloWorldLoader from "./loaders/hello-world" -For example, resolve the `baseRepository` in your service's constructor: +export default Module("hello", { + // ... + loaders: [helloWorldLoader], +}) +``` -### Extending Service Factory +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. -```ts highlights={[["14"]]} +### 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! +``` + +This indicates that the loader in the `hello` module ran and logged this message. + +*** + +## Example: Register Custom MongoDB Connection + +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. + +Consider your have a MongoDB module that allows you to perform operations on a MongoDB database. + +### Prerequisites + +- [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) + +To connect to the database, you create the following loader in your module: + +```ts title="src/modules/mongo/loaders/connection.ts" highlights={loaderHighlights} +import { LoaderOptions } from "@medusajs/framework/types" +import { asValue } from "awilix" +import { MongoClient } from "mongodb" + +type ModuleOptions = { + connection_url?: string + db_name?: string +} + +export default async function mongoConnectionLoader({ + container, + options, +}: LoaderOptions<ModuleOptions>) { + 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) + + logger.info("Connected to MongoDB") + + 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: + +```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: + +- `connection_url`: the URL to connect to the MongoDB database. +- `db_name`: The name of the database to connect to. + +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. + +After creating the client, you register it in the module's container using the container's `register` method. The method accepts two parameters: + +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. + +### Use Custom Registered Resource in Module's Service + +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: + +```ts title="src/modules/mongo/service.ts" +import type { Db } from "mongodb" + +type InjectedDependencies = { + mongoClient: Db +} + +export default class MongoModuleService { + private mongoClient_: Db + + constructor({ mongoClient }: InjectedDependencies) { + this.mongoClient_ = mongoClient + } + + async createMovie({ title }: { + title: string + }) { + const moviesCol = this.mongoClient_.collection("movie") + + const insertedMovie = await moviesCol.insertOne({ + title, + }) + + const movie = await moviesCol.findOne({ + _id: insertedMovie.insertedId, + }) + + return movie + } + + async deleteMovie(id: string) { + const moviesCol = this.mongoClient_.collection("movie") + + await moviesCol.deleteOne({ + _id: { + equals: id, + }, + }) + } +} +``` + +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. + +Make sure to export the loader in the module's definition in the `index.ts` file at the root directory of the module: + +```ts title="src/modules/mongo/index.ts" highlights={[["9"]]} +import { Module } from "@medusajs/framework/utils" +import MongoModuleService from "./service" +import mongoConnectionLoader from "./loaders/connection" + +export const MONGO_MODULE = "mongo" + +export default Module(MONGO_MODULE, { + service: MongoModuleService, + loaders: [mongoConnectionLoader], +}) +``` + +### Test it Out + +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 +info: Connected to MongoDB +``` + +You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database. + + +# 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. + +## Run Queries + +[MikroORM's entity manager](https://mikro-orm.io/docs/entity-manager) is a class that has methods to run queries on the database and perform operations. + +Medusa provides an `InjectManager` decorator 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). + +So, to run database queries in a service: + +1. Add the `InjectManager` decorator to the method. +2. Add as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator from the Modules SDK. This context holds database-related context, including the manager injected by `InjectManager` + +For example, in your service, add the following methods: + +```ts highlights={methodsHighlight} +// other imports... +import { + InjectManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { SqlEntityManager } from "@mikro-orm/knex" + +class HelloModuleService { + // ... + + @InjectManager() + async getCount( + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<number> { + return await sharedContext.manager.count("my_custom") + } + + @InjectManager() + async getCountSql( + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<number> { + const data = await sharedContext.manager.execute( + "SELECT COUNT(*) as num FROM my_custom" + ) + + return parseInt(data[0].num) + } +} +``` + +You add two methods `getCount` and `getCountSql` that have the `InjectManager` decorator. Each of the methods also accept the `sharedContext` parameter which has the `MedusaContext` decorator. + +The entity manager is injected to the `sharedContext.manager` property, which is an instance of [EntityManager from the @mikro-orm/knex package](https://mikro-orm.io/api/knex/class/EntityManager). + +You use the manager in the `getCount` method to retrieve the number of records in a table, and in the `getCountSql` to run a PostgreSQL query that retrieves the count. + +Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. + +*** + +## Execute Operations in Transactions + +To wrap database operations in a transaction, you create two methods: + +1. A private or protected method that's wrapped in a transaction. To wrap it in a transaction, you use the `InjectTransactionManager` decorator 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. + +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 highlights={opHighlights} +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" + +class HelloModuleService { + // ... + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<any> { + const transactionManager = sharedContext.transactionManager + await transactionManager.nativeUpdate( + "my_custom", + { + id: input.id, + }, + { + name: input.name, + } + ) + + // retrieve again + const updatedRecord = await transactionManager.execute( + `SELECT * FROM my_custom WHERE id = '${input.id}'` + ) + + return updatedRecord + } + + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ) { + return await this.update_(input, sharedContext) + } +} +``` + +The `HelloModuleService` has two methods: + +- A protected `update_` that performs the database operations inside a transaction. +- A public `update` that executes the transactional protected method. + +The shared context's `transactionManager` property holds the transactional entity manager (injected by `InjectTransactionManager`) that you use to perform database operations. + +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 + +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. + +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. + +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. + +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<EntityManager> + ) { + const newData = await this.update_(input, sharedContext) + + await sendNewDataToSystem(newData) + + return newData + } +} +``` + +In this case, only the `update_` method is wrapped in a transaction. The returned value `newData` holds the committed result, which can be used for other operations, such as passed to a `sendNewDataToSystem` method. + +### Using Methods in Transactional Methods + +If your transactional method uses other methods that accept a Medusa context, pass the shared context to those methods. + +For example: + +```ts +// other imports... +import { EntityManager } from "@mikro-orm/knex" + +class HelloModuleService { + // ... + @InjectTransactionManager() + protected async anotherMethod( + @MedusaContext() sharedContext?: Context<EntityManager> + ) { + // ... + } + + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<any> { + this.anotherMethod(sharedContext) + } +} +``` + +You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context. + +The `anotherMethod` now runs in the same transaction as the `update_` method. + +*** + +## Configure Transactions + +To configure the transaction, such as its [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html), use the `baseRepository` dependency registered in your module's container. + +The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more. + +The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction. + +For example, resolve the `baseRepository` in your service's constructor: + +### Extending Service Factory + +```ts highlights={[["14"]]} import { MedusaService } from "@medusajs/framework/utils" import MyCustom from "./models/my-custom" import { DAL } from "@medusajs/framework/types" @@ -10610,468 +11294,86 @@ class HelloModuleService { ``` -# Module Isolation - -In this chapter, you'll learn how modules are isolated, and what that means for your custom development. +# Modules Directory Structure -- 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. +In this document, you'll learn about the expected files and directories in your module. -## How are Modules Isolated? + -A module is unaware of any resources other than its own, such as services or data models. This means it can't access these resources if they're implemented in another module. +## index.ts -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. +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). *** -## Why are Modules Isolated - -Some of the module isolation's benefits include: +## service.ts -- 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. +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). *** -## How to Extend Data Model of Another Module? - -To extend the data model of another module, such as the `product` data model of the Product Module, use Medusa's linking concepts as explained in the [Module Links chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). - -*** +## Other Directories -## How to Use Services of Other Modules? +The following directories are optional and their content are explained more in the following chapters: -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. +- `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. -Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. -### Example +# Multiple Services in a Module -For example, consider you have two modules: +In this chapter, you'll learn how to use multiple services in a module. -1. A module that stores and manages brands in your application. -2. A module that integrates a third-party Content Management System (CMS). +## Module's Main and Internal Services -To sync brands from your application to the third-party system, create the following steps: +A module has one main service only, which is the service exported in the module's definition. -```ts title="Example Steps" highlights={stepsHighlights} -const retrieveBrandsStep = createStep( - "retrieve-brands", - async (_, { container }) => { - const brandModuleService = container.resolve( - "brandModuleService" - ) +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. - const brands = await brandModuleService.listBrands() +*** - return new StepResponse(brands) - } -) +## How to Add an Internal Service -const createBrandsInCmsStep = createStep( - "create-brands-in-cms", - async ({ brands }, { container }) => { - const cmsModuleService = container.resolve( - "cmsModuleService" - ) +### 1. Create Service - const cmsBrands = await cmsModuleService.createBrands(brands) +To add an internal service, create it in the `services` directory of your module. - return new StepResponse(cmsBrands, cmsBrands) - }, - async (brands, { container }) => { - const cmsModuleService = container.resolve( - "cmsModuleService" - ) +For example, create the file `src/modules/hello/services/client.ts` with the following content: - await cmsModuleService.deleteBrands( - brands.map((brand) => brand.id) - ) +```ts title="src/modules/hello/services/client.ts" +export class ClientService { + async getMessage(): Promise<string> { + return "Hello, World!" } -) +} ``` -The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module. +### 2. Export Service in Index -Then, create the following workflow that uses these steps: +Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. -```ts title="Example Workflow" -export const syncBrandsWorkflow = createWorkflow( - "sync-brands", - () => { - const brands = retrieveBrandsStep() +For example, create the file `src/modules/hello/services/index.ts` with the following content: - createBrandsInCmsStep({ brands }) - } -) +```ts title="src/modules/hello/services/index.ts" +export * from "./client" ``` -You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. - - -# Loaders +This exports the `ClientService`. -In this chapter, you’ll learn about loaders and how to use them. +### 3. Resolve Internal Service -## What is a Loader? +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. -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. +For example, in your main service: -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. +```ts title="src/modules/hello/service.ts" highlights={[["5"], ["13"]]} +// other imports... +import { ClientService } from "./services" -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. - -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). - -*** - -## How to Create a Loader? - -### 1. Implement Loader Function - -You create a loader function in a TypeScript or JavaScript file under a module's `loaders` directory. - -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: - - - -Learn how to create a module in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -```ts title="src/modules/hello/loaders/hello-world.ts" -import { - LoaderOptions, -} from "@medusajs/framework/types" - -export default async function helloWorldLoader({ - container, -}: LoaderOptions) { - const logger = container.resolve("logger") - - logger.info("[helloWorldLoader]: Hello, World!") -} -``` - -The loader file exports an async function, which is the function executed when the application loads. - -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. - -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). - -### 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], -}) -``` - -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! -``` - -This indicates that the loader in the `hello` module ran and logged this message. - -*** - -## Example: Register Custom MongoDB Connection - -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. - -Consider your have a MongoDB module that allows you to perform operations on a MongoDB database. - -### Prerequisites - -- [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) - -To connect to the database, you create the following loader in your module: - -```ts title="src/modules/mongo/loaders/connection.ts" highlights={loaderHighlights} -import { LoaderOptions } from "@medusajs/framework/types" -import { asValue } from "awilix" -import { MongoClient } from "mongodb" - -type ModuleOptions = { - connection_url?: string - db_name?: string -} - -export default async function mongoConnectionLoader({ - container, - options, -}: LoaderOptions<ModuleOptions>) { - 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) - - logger.info("Connected to MongoDB") - - 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: - -```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: - -- `connection_url`: the URL to connect to the MongoDB database. -- `db_name`: The name of the database to connect to. - -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. - -After creating the client, you register it in the module's container using the container's `register` method. The method accepts two parameters: - -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. - -### Use Custom Registered Resource in Module's Service - -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: - -```ts title="src/modules/mongo/service.ts" -import type { Db } from "mongodb" - -type InjectedDependencies = { - mongoClient: Db -} - -export default class MongoModuleService { - private mongoClient_: Db - - constructor({ mongoClient }: InjectedDependencies) { - this.mongoClient_ = mongoClient - } - - async createMovie({ title }: { - title: string - }) { - const moviesCol = this.mongoClient_.collection("movie") - - const insertedMovie = await moviesCol.insertOne({ - title, - }) - - const movie = await moviesCol.findOne({ - _id: insertedMovie.insertedId, - }) - - return movie - } - - async deleteMovie(id: string) { - const moviesCol = this.mongoClient_.collection("movie") - - await moviesCol.deleteOne({ - _id: { - equals: id, - }, - }) - } -} -``` - -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. - -Make sure to export the loader in the module's definition in the `index.ts` file at the root directory of the module: - -```ts title="src/modules/mongo/index.ts" highlights={[["9"]]} -import { Module } from "@medusajs/framework/utils" -import MongoModuleService from "./service" -import mongoConnectionLoader from "./loaders/connection" - -export const MONGO_MODULE = "mongo" - -export default Module(MONGO_MODULE, { - service: MongoModuleService, - loaders: [mongoConnectionLoader], -}) -``` - -### Test it Out - -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 -info: Connected to MongoDB -``` - -You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database. - - -# Modules Directory Structure - -In this document, you'll learn about the expected files and directories in your module. - - - -## index.ts - -The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -*** - -## service.ts - -A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -*** - -## Other Directories - -The following directories are optional and their content are explained more in the following chapters: - -- `models`: Holds the data models representing tables in the database. -- `migrations`: Holds the migration files used to reflect changes on the database. -- `loaders`: Holds the scripts to run on the Medusa application's start-up. - - -# Service Constraints - -This chapter lists constraints to keep in mind when creating a service. - -## Use Async Methods - -Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronous, it always executes methods in the wrapper with the `await` keyword. - -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() -``` - -So, make sure your service's methods are always async to avoid unexpected errors or behavior. - -```ts highlights={[["8", "", "Method must be async."], ["13", "async", "Correct way of defining the method."]]} -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" - -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - // Don't - getMessage(): string { - return "Hello, World!" - } - - // Do - async getMessage(): Promise<string> { - return "Hello, World!" - } -} - -export default HelloModuleService -``` - - -# Multiple Services in a Module - -In this chapter, you'll learn how to use multiple services in a module. - -## Module's Main and Internal Services - -A module has one main service only, which is the service exported in the module's definition. - -However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources. - -*** - -## How to Add an Internal Service - -### 1. Create Service - -To add an internal service, create it in the `services` directory of your module. - -For example, create the file `src/modules/hello/services/client.ts` with the following content: - -```ts title="src/modules/hello/services/client.ts" -export class ClientService { - async getMessage(): Promise<string> { - return "Hello, World!" - } -} -``` - -### 2. Export Service in Index - -Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. - -For example, create the file `src/modules/hello/services/index.ts` with the following content: - -```ts title="src/modules/hello/services/index.ts" -export * from "./client" -``` - -This exports the `ClientService`. - -### 3. Resolve Internal Service - -Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders. - -For example, in your main service: - -```ts title="src/modules/hello/service.ts" highlights={[["5"], ["13"]]} -// other imports... -import { ClientService } from "./services" - -type InjectedDependencies = { - clientService: ClientService -} +type InjectedDependencies = { + clientService: ClientService +} class HelloModuleService extends MedusaService({ MyCustom, @@ -11147,6 +11449,44 @@ The `configModule` has a `modules` property that includes all registered modules If its value is not a `boolean`, set the service's options to the module configuration's `options` property. +# Service Constraints + +This chapter lists constraints to keep in mind when creating a service. + +## Use Async Methods + +Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronous, it always executes methods in the wrapper with the `await` keyword. + +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() +``` + +So, make sure your service's methods are always async to avoid unexpected errors or behavior. + +```ts highlights={[["8", "", "Method must be async."], ["13", "async", "Correct way of defining the method."]]} +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" + +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + // Don't + getMessage(): string { + return "Hello, World!" + } + + // Do + async getMessage(): Promise<string> { + return "Hello, World!" + } +} + +export default HelloModuleService +``` + + # Module Options 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. @@ -11485,36 +11825,6 @@ export default HelloModuleService ``` -# Scheduled Jobs Number of Executions - -In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. - -## numberOfExecutions Option - -The export configuration object of the scheduled job accepts an optional property `numberOfExecutions`. Its value is a number indicating how many times the scheduled job can be executed during the Medusa application's runtime. - -For example: - -```ts highlights={highlights} -export default async function myCustomJob() { - console.log("I'll be executed three times only.") -} - -export const config = { - name: "hello-world", - // execute every minute - schedule: "* * * * *", - numberOfExecutions: 3, -} -``` - -The above scheduled job has the `numberOfExecutions` configuration set to `3`. - -So, it'll only execute 3 times, each every minute, then it won't be executed anymore. - -If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. - - # Access Workflow Errors In this chapter, you’ll learn how to access errors that occur during a workflow’s execution. @@ -11629,6 +11939,36 @@ The hook is available on the workflow's `hooks` property using its name `product You invoke the hook, passing a step function (the hook handler) as a parameter. +# Scheduled Jobs Number of Executions + +In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. + +## numberOfExecutions Option + +The export configuration object of the scheduled job accepts an optional property `numberOfExecutions`. Its value is a number indicating how many times the scheduled job can be executed during the Medusa application's runtime. + +For example: + +```ts highlights={highlights} +export default async function myCustomJob() { + console.log("I'll be executed three times only.") +} + +export const config = { + name: "hello-world", + // execute every minute + schedule: "* * * * *", + numberOfExecutions: 3, +} +``` + +The above scheduled job has the `numberOfExecutions` configuration set to `3`. + +So, it'll only execute 3 times, each every minute, then it won't be executed anymore. + +If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. + + # Compensation Function In this chapter, you'll learn what a compensation function is and how to add it to a step. @@ -12968,6 +13308,60 @@ By setting `retryInterval` on a step, a workflow becomes a [long-running workflo 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). +# Run Workflow Steps in Parallel + +In this chapter, you’ll learn how to run workflow steps in parallel. + +## parallelize Utility Function + +If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK. + +The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step. + +For example: + +```ts highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" +import { + createWorkflow, + WorkflowResponse, + parallelize, +} from "@medusajs/framework/workflows-sdk" +import { + createProductStep, + getProductStep, + createPricesStep, + attachProductToSalesChannelStep, +} from "./steps" + +interface WorkflowInput { + title: string +} + +const myWorkflow = createWorkflow( + "my-workflow", + (input: WorkflowInput) => { + const product = createProductStep(input) + + const [prices, productSalesChannel] = parallelize( + createPricesStep(product), + attachProductToSalesChannelStep(product) + ) + + const id = product.id + const refetchedProduct = getProductStep(product.id) + + return new WorkflowResponse(refetchedProduct) + } +) +``` + +The `parallelize` function accepts the steps to run in parallel as a parameter. + +It returns an array of the steps' results in the same order they're passed to the `parallelize` function. + +So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`. + + # Store Workflow Executions In this chapter, you'll learn how to store workflow executions in the database and access them later. @@ -13113,58 +13507,90 @@ if (workflowExecution.state === "failed") { Other state values include `done`, `invoking`, and `compensating`. -# Run Workflow Steps in Parallel +# Workflow Timeout -In this chapter, you’ll learn how to run workflow steps in parallel. +In this chapter, you’ll learn how to set a timeout for workflows and steps. -## parallelize Utility Function +## What is a Workflow Timeout? -If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK. +By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. -The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step. +You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown. + +### Timeout Doesn't Stop Step Execution + +Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result. + +*** + +## Configure Workflow Timeout + +The `createWorkflow` function can accept a configuration object instead of the workflow’s name. + +In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. For example: -```ts highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" -import { +```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" +import { + createStep, createWorkflow, WorkflowResponse, - parallelize, } from "@medusajs/framework/workflows-sdk" -import { - createProductStep, - getProductStep, - createPricesStep, - attachProductToSalesChannelStep, -} from "./steps" -interface WorkflowInput { - title: string -} +const step1 = createStep( + "step-1", + async () => { + // ... + } +) -const myWorkflow = createWorkflow( - "my-workflow", - (input: WorkflowInput) => { - const product = createProductStep(input) +const myWorkflow = createWorkflow({ + name: "hello-world", + timeout: 2, // 2 seconds +}, function () { + const str1 = step1() - const [prices, productSalesChannel] = parallelize( - createPricesStep(product), - attachProductToSalesChannelStep(product) - ) + return new WorkflowResponse({ + message: str1, + }) +}) - const id = product.id - const refetchedProduct = getProductStep(product.id) +export default myWorkflow - return new WorkflowResponse(refetchedProduct) - } -) ``` -The `parallelize` function accepts the steps to run in parallel as a parameter. +This workflow's executions fail if they run longer than two seconds. -It returns an array of the steps' results in the same order they're passed to the `parallelize` function. +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`. -So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`. +*** + +## Configure Step Timeout + +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. + +The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. + +For example: + +```tsx +const step1 = createStep( + { + name: "step-1", + timeout: 2, // 2 seconds + }, + async () => { + // ... + } +) +``` + +This step's executions fail if they run longer than two seconds. + +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`. # Workflow Hooks @@ -13496,105 +13922,34 @@ const myWorkflow = createWorkflow( ``` -# Workflow Timeout - -In this chapter, you’ll learn how to set a timeout for workflows and steps. - -## What is a Workflow Timeout? - -By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. - -You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown. - -### Timeout Doesn't Stop Step Execution - -Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result. +# 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. -## Configure Workflow Timeout +### Prerequisites -The `createWorkflow` function can accept a configuration object instead of the workflow’s name. +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. +## Write Integration Test for Module -For example: +Consider a `hello` module with a `HelloModuleService` that has a `getMessage` method: -```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" -import { - createStep, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +```ts title="src/modules/hello/service.ts" +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" -const step1 = createStep( - "step-1", - async () => { - // ... +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + getMessage(): string { + return "Hello, World!" } -) - -const myWorkflow = createWorkflow({ - name: "hello-world", - timeout: 2, // 2 seconds -}, function () { - const str1 = step1() - - return new WorkflowResponse({ - message: str1, - }) -}) - -export default myWorkflow - -``` - -This workflow's executions fail if they run longer than two seconds. - -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`. - -*** - -## Configure Step Timeout - -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. - -The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. - -For example: +} -```tsx -const step1 = createStep( - { - name: "step-1", - timeout: 2, // 2 seconds - }, - async () => { - // ... - } -) +export default HelloModuleService ``` -This step's executions fail if they run longer than two seconds. - -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`. - - -# Write Tests for Modules - -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) - -## moduleIntegrationTestRunner Utility - -`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. - -For example, assuming you have a `hello` module, create a test file at `src/modules/hello/__tests__/service.spec.ts`: +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" @@ -13607,29 +13962,24 @@ moduleIntegrationTestRunner<HelloModuleService>({ moduleModels: [MyCustom], resolve: "./src/modules/hello", testSuite: ({ service }) => { - // TODO write tests + describe("HelloModuleService", () => { + it("says hello world", () => { + const message = service.getMessage() + + expect(message).toEqual("Hello, World!") + }) + }) }, }) jest.setTimeout(60 * 1000) ``` -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/). +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 Tests +## Run Test Run the following command to run your module integration tests: @@ -13641,116 +13991,77 @@ 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" +# Example: Write Integration Tests for Workflows -moduleIntegrationTestRunner<HelloModuleService>({ - moduleOptions: { - apiKey: "123", - }, - // ... -}) -``` +In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framwork. -*** +### Prerequisites -## Write Tests for Modules without Data Models +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -If your module doesn't have a data model, pass a dummy model in the `moduleModels` property. +## Write Integration Test for Workflow -For example: +Consider you have the following workflow defined at `src/workflows/hello-world.ts`: -```ts -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import HelloModuleService from "../service" -import { model } from "@medusajs/framework/utils" +```ts title="src/workflows/hello-world.ts" +import { + createWorkflow, + createStep, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -const DummyModel = model.define("dummy_model", { - id: model.id().primaryKey(), +const step1 = createStep("step-1", () => { + return new StepResponse("Hello, World!") }) -moduleIntegrationTestRunner<HelloModuleService>({ - moduleModels: [DummyModel], - // ... -}) +export const helloWorldWorkflow = createWorkflow( + "hello-world-workflow", + () => { + const message = step1() -jest.setTimeout(60 * 1000) + return new WorkflowResponse(message) + } +) ``` -*** - -### Other Options and Inputs - -Refer to [this reference in the Development Resources documentation](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 [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). - - -# Write Integration Tests - -In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests. - -### 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. - -For example: +To write a test for this workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content: -```ts title="integration-tests/http/test.spec.ts" highlights={highlights} +```ts title="integration-tests/http/workflow.spec.ts" import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { helloWorldWorkflow } from "../../src/workflows/hello-world" medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - // TODO write tests... + testSuite: ({ getContainer }) => { + describe("Test hello-world workflow", () => { + it("returns message", async () => { + const { result } = await helloWorldWorkflow(getContainer()) + .run() + + expect(result).toEqual("Hello, World!") + }) + }) }, }) jest.setTimeout(60 * 1000) ``` -The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`. - -`testSuite`'s value is a function that defines the tests to run. The function accepts as a parameter an object that has the following properties: - -- `api`: a set of utility methods used to send requests to the Medusa application. It has the following methods: - - `get`: Send a `GET` request to an API route. - - `post`: Send a `POST` request to an API route. - - `delete`: Send a `DELETE` request to an API route. -- `getContainer`: a function that retrieves the Medusa Container. Use the `getContainer().resolve` method to resolve resources from the Medusa Container. - -The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). +You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`. ### 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" +```ts title="integration-tests/http/custom-routes.spec.ts" // in your test's file jest.setTimeout(60 * 1000) ``` *** -### Run Tests +## Run Test Run the following command to run your tests: @@ -13760,74 +14071,45 @@ 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 - -Refer to [this reference in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. - -*** - -## Database Used in Tests - -The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. - -To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md). +This runs your Medusa application and runs the tests available under the `integrations/http` directory. *** -## Example Integration Tests - -The next chapters provide examples of writing integration tests for API routes and workflows. - - -# 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) +## Test That a Workflow Throws an Error -## Write Integration Test for Module +You might want to test that a workflow throws an error in certain cases. To test this: -Consider a `hello` module with a `HelloModuleService` that has a `getMessage` method: +- Disable the `throwOnError` option when executing the workflow. +- Use the returned `errors` property to check what errors were thrown. -```ts title="src/modules/hello/service.ts" -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" +For example, if you have a step that throws this error: -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - getMessage(): string { - return "Hello, World!" - } -} +```ts title="src/workflows/hello-world.ts" +import { MedusaError } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" -export default HelloModuleService +const step1 = createStep("step-1", () => { + throw new MedusaError(MedusaError.Types.NOT_FOUND, "Item doesn't exist") +}) ``` -To create an integration test for the method, create the file `src/modules/hello/__tests__/service.spec.ts` with the following content: +You can write the following test to ensure that the workflow throws that error: -```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" +```ts title="integration-tests/http/workflow.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { helloWorldWorkflow } from "../../src/workflows/hello-world" -moduleIntegrationTestRunner<HelloModuleService>({ - moduleName: HELLO_MODULE, - moduleModels: [MyCustom], - resolve: "./src/modules/hello", - testSuite: ({ service }) => { - describe("HelloModuleService", () => { - it("says hello world", () => { - const message = service.getMessage() +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test hello-world workflow", () => { + it("returns message", async () => { + const { errors } = await helloWorldWorkflow(getContainer()) + .run({ + throwOnError: false, + }) - expect(message).toEqual("Hello, World!") + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].error.message).toBe("Item doesn't exist") }) }) }, @@ -13836,21 +14118,9 @@ moduleIntegrationTestRunner<HelloModuleService>({ 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). +The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, being the error thrown. -This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. +If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. # Example: Write Integration Tests for API Routes @@ -14419,137 +14689,6 @@ const response = await api.post(`/custom`, form, { ``` -# Example: Write Integration Tests for Workflows - -In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framwork. - -### Prerequisites - -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) - -## Write Integration Test for Workflow - -Consider you have the following workflow defined at `src/workflows/hello-world.ts`: - -```ts title="src/workflows/hello-world.ts" -import { - createWorkflow, - createStep, - StepResponse, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep("step-1", () => { - return new StepResponse("Hello, World!") -}) - -export const helloWorldWorkflow = createWorkflow( - "hello-world-workflow", - () => { - const message = step1() - - return new WorkflowResponse(message) - } -) -``` - -To write a test for this workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content: - -```ts title="integration-tests/http/workflow.spec.ts" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { helloWorldWorkflow } from "../../src/workflows/hello-world" - -medusaIntegrationTestRunner({ - testSuite: ({ getContainer }) => { - describe("Test hello-world workflow", () => { - it("returns message", async () => { - const { result } = await helloWorldWorkflow(getContainer()) - .run() - - expect(result).toEqual("Hello, World!") - }) - }) - }, -}) - -jest.setTimeout(60 * 1000) -``` - -You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`. - -### 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/custom-routes.spec.ts" -// in your test's file -jest.setTimeout(60 * 1000) -``` - -*** - -## Run Test - -Run the following command to run your tests: - -```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 `integrations/http` directory. - -*** - -## Test That a Workflow Throws an Error - -You might want to test that a workflow throws an error in certain cases. To test this: - -- Disable the `throwOnError` option when executing the workflow. -- Use the returned `errors` property to check what errors were thrown. - -For example, if you have a step that throws this error: - -```ts title="src/workflows/hello-world.ts" -import { MedusaError } from "@medusajs/framework/utils" -import { createStep } from "@medusajs/framework/workflows-sdk" - -const step1 = createStep("step-1", () => { - throw new MedusaError(MedusaError.Types.NOT_FOUND, "Item doesn't exist") -}) -``` - -You can write the following test to ensure that the workflow throws that error: - -```ts title="integration-tests/http/workflow.spec.ts" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { helloWorldWorkflow } from "../../src/workflows/hello-world" - -medusaIntegrationTestRunner({ - testSuite: ({ getContainer }) => { - describe("Test hello-world workflow", () => { - it("returns message", async () => { - const { errors } = await helloWorldWorkflow(getContainer()) - .run({ - throwOnError: false, - }) - - expect(errors.length).toBeGreaterThan(0) - expect(errors[0].error.message).toBe("Item doesn't exist") - }) - }) - }, -}) - -jest.setTimeout(60 * 1000) -``` - -The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, being the error thrown. - -If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. - - # Commerce Modules In this section of the documentation, you'll find guides and references related to Medusa's commerce modules. @@ -14571,136 +14710,6 @@ The Commerce Modules can be used in many use cases, including: - Node.js Application: Use the Commerce Modules in any Node.js application by installing it with NPM. -# Auth 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. - -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). - -## Auth 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. - -*** - -## 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. - -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/authenticate-user.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 -} - -const authenticateUserStep = createStep( - "authenticate-user", - async ({ req }: Input, { container }) => { - const authModuleService = container.resolve(Modules.AUTH) - - 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" - ) - } - - 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 { authenticateUserWorkflow } from "../../workflows/authenticate-user" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await authenticateUserWorkflow(req.scope) - .run({ - req, - }) - - res.send(result) -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - -## Configure Auth Module - -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. - -*** - -## Providers - -Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types. - -*** - - # API Key Module In this section of the documentation, you will find resources to learn more about the API Key Module and how to use it in your application. @@ -14991,24 +15000,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Customer Module - -In this section of the documentation, you will find resources to learn more about the Customer Module and how to use it in your application. +# Auth Module -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers and groups 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 customer related features available out-of-the-box through the Customer 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 Customer 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). -## Customer Features +## Auth Features -- [Customer Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/customer-accounts/index.html.md): Store and manage guest and registered customers in your store. -- [Customer Organization](https://docs.medusajs.com/references/customer/models/index.html.md): Organize customers into groups. This has a lot of benefits and supports many use cases, such as provide discounts for specific customer groups using the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md). +- [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 Customer 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. @@ -15016,45 +15025,67 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-customer.ts" highlights={highlights} +```ts title="src/workflows/authenticate-user.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, createStep, StepResponse, } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" +import { Modules, MedusaError } from "@medusajs/framework/utils" +import { MedusaRequest } from "@medusajs/framework/http" +import { AuthenticationInput } from "@medusajs/framework/types" -const createCustomerStep = createStep( - "create-customer", - async ({}, { container }) => { - const customerModuleService = container.resolve(Modules.CUSTOMER) +type Input = { + req: MedusaRequest +} - const customer = await customerModuleService.createCustomers({ - first_name: "Peter", - last_name: "Hayes", - email: "peter.hayes@example.com", - }) +const authenticateUserStep = createStep( + "authenticate-user", + async ({ req }: Input, { container }) => { + const authModuleService = container.resolve(Modules.AUTH) - return new StepResponse({ customer }, customer.id) + 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" + ) + } + + return new StepResponse({ authIdentity }, authIdentity?.id) }, - async (customerId, { container }) => { - if (!customerId) { + async (authIdentityId, { container }) => { + if (!authIdentityId) { return } - const customerModuleService = container.resolve(Modules.CUSTOMER) + + const authModuleService = container.resolve(Modules.AUTH) - await customerModuleService.deleteCustomers([customerId]) + await authModuleService.deleteAuthIdentities([authIdentityId]) } ) -export const createCustomerWorkflow = createWorkflow( - "create-customer", - () => { - const { customer } = createCustomerStep() +export const authenticateUserWorkflow = createWorkflow( + "authenticate-user", + (input: Input) => { + const { authIdentity } = authenticateUserStep(input) return new WorkflowResponse({ - customer, + authIdentity, }) } ) @@ -15062,97 +15093,61 @@ export const createCustomerWorkflow = createWorkflow( You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +```ts title="API Route" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createCustomerWorkflow } from "../../workflows/create-customer" +import { authenticateUserWorkflow } from "../../workflows/authenticate-user" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createCustomerWorkflow(req.scope) - .run() + const { result } = await authenticateUserWorkflow(req.scope) + .run({ + req, + }) res.send(result) } ``` -### 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 { createCustomerWorkflow } from "../workflows/create-customer" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createCustomerWorkflow(container) - .run() - - 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"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createCustomerWorkflow } from "../workflows/create-customer" +## Configure Auth Module -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createCustomerWorkflow(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 = { - 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. *** -# Inventory Module +# Customer Module -In this section of the documentation, you will find resources to learn more about the Inventory Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Customer Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers and groups using the dashboard. -Medusa has inventory related features available out-of-the-box through the Inventory 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 Inventory Module. +Medusa has customer related features available out-of-the-box through the Customer 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 Customer Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Inventory Features +## Customer Features -- [Inventory Items Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md): Store and manage inventory of any stock-kept item, such as product variants. -- [Inventory Across Locations](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventorylevel/index.html.md): Manage inventory levels across different locations, such as warehouses. -- [Reservation Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#reservationitem/index.html.md): Reserve quantities of inventory items at specific locations for orders or other purposes. -- [Check Inventory Availability](https://docs.medusajs.com/references/inventory-next/confirmInventory/index.html.md): Check whether an inventory item has the necessary quantity for purchase. -- [Inventory Kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. +- [Customer Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/customer-accounts/index.html.md): Store and manage guest and registered customers in your store. +- [Customer Organization](https://docs.medusajs.com/references/customer/models/index.html.md): Organize customers into groups. This has a lot of benefits and supports many use cases, such as provide discounts for specific customer groups using the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md). *** -## How to Use the Inventory Module +## How to Use the Customer 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. @@ -15160,7 +15155,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-inventory-item.ts" highlights={highlights} +```ts title="src/workflows/create-customer.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -15169,36 +15164,36 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createInventoryItemStep = createStep( - "create-inventory-item", +const createCustomerStep = createStep( + "create-customer", async ({}, { container }) => { - const inventoryModuleService = container.resolve(Modules.INVENTORY) + const customerModuleService = container.resolve(Modules.CUSTOMER) - const inventoryItem = await inventoryModuleService.createInventoryItems({ - sku: "SHIRT", - title: "Green Medusa Shirt", - requires_shipping: true, + const customer = await customerModuleService.createCustomers({ + first_name: "Peter", + last_name: "Hayes", + email: "peter.hayes@example.com", }) - return new StepResponse({ inventoryItem }, inventoryItem.id) + return new StepResponse({ customer }, customer.id) }, - async (inventoryItemId, { container }) => { - if (!inventoryItemId) { + async (customerId, { container }) => { + if (!customerId) { return } - const inventoryModuleService = container.resolve(Modules.INVENTORY) + const customerModuleService = container.resolve(Modules.CUSTOMER) - await inventoryModuleService.deleteInventoryItems([inventoryItemId]) + await customerModuleService.deleteCustomers([customerId]) } ) -export const createInventoryItemWorkflow = createWorkflow( - "create-inventory-item-workflow", +export const createCustomerWorkflow = createWorkflow( + "create-customer", () => { - const { inventoryItem } = createInventoryItemStep() + const { customer } = createCustomerStep() return new WorkflowResponse({ - inventoryItem, + customer, }) } ) @@ -15213,13 +15208,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" +import { createCustomerWorkflow } from "../../workflows/create-customer" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createInventoryItemWorkflow(req.scope) + const { result } = await createCustomerWorkflow(req.scope) .run() res.send(result) @@ -15233,13 +15228,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" +import { createCustomerWorkflow } from "../workflows/create-customer" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createInventoryItemWorkflow(container) + const { result } = await createCustomerWorkflow(container) .run() console.log(result) @@ -15254,12 +15249,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" +import { createCustomerWorkflow } from "../workflows/create-customer" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createInventoryItemWorkflow(container) + const { result } = await createCustomerWorkflow(container) .run() console.log(result) @@ -15590,27 +15585,27 @@ The Fulfillment Module accepts options for further configurations. Refer to [thi *** -# Order Module +# Inventory Module -In this section of the documentation, you will find resources to learn more about the Order Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Inventory Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/index.html.md) to learn how to manage orders using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features using the dashboard. -Medusa has order related features available out-of-the-box through the Order 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 Order Module. +Medusa has inventory related features available out-of-the-box through the Inventory 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 Inventory Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Order Features +## Inventory Features -- [Order Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/concepts/index.html.md): Store and manage your orders to retrieve, create, cancel, and perform other operations. -- Draft Orders: Allow merchants to create orders on behalf of their customers as draft orders that later are transformed to regular orders. -- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/promotion-adjustments/index.html.md): Apply promotions or discounts to the order's 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/order/tax-lines/index.html.md): Apply tax lines to an order's line items and shipping methods. -- [Returns](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md), [Edits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md), [Exchanges](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md), and [Claims](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md): Make [changes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to an order to edit, return, or exchange its items, with [version-based control](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) over the order's timeline. +- [Inventory Items Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md): Store and manage inventory of any stock-kept item, such as product variants. +- [Inventory Across Locations](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventorylevel/index.html.md): Manage inventory levels across different locations, such as warehouses. +- [Reservation Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#reservationitem/index.html.md): Reserve quantities of inventory items at specific locations for orders or other purposes. +- [Check Inventory Availability](https://docs.medusajs.com/references/inventory-next/confirmInventory/index.html.md): Check whether an inventory item has the necessary quantity for purchase. +- [Inventory Kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. *** -## How to Use the Order Module +## How to Use the Inventory 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. @@ -15618,7 +15613,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-draft-order.ts" highlights={highlights} +```ts title="src/workflows/create-inventory-item.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -15627,26 +15622,170 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createDraftOrderStep = createStep( - "create-order", +const createInventoryItemStep = createStep( + "create-inventory-item", async ({}, { container }) => { - const orderModuleService = container.resolve(Modules.ORDER) + const inventoryModuleService = container.resolve(Modules.INVENTORY) - const draftOrder = await orderModuleService.createOrders({ - currency_code: "usd", - items: [ - { - title: "Shirt", - quantity: 1, - unit_price: 3000, - }, - ], - shipping_methods: [ - { - name: "Express shipping", - amount: 3000, - }, - ], + const inventoryItem = await inventoryModuleService.createInventoryItems({ + sku: "SHIRT", + title: "Green Medusa Shirt", + requires_shipping: true, + }) + + return new StepResponse({ inventoryItem }, inventoryItem.id) + }, + async (inventoryItemId, { container }) => { + if (!inventoryItemId) { + return + } + const inventoryModuleService = container.resolve(Modules.INVENTORY) + + await inventoryModuleService.deleteInventoryItems([inventoryItemId]) + } +) + +export const createInventoryItemWorkflow = createWorkflow( + "create-inventory-item-workflow", + () => { + const { inventoryItem } = createInventoryItemStep() + + return new WorkflowResponse({ + inventoryItem, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### 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 { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createInventoryItemWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### 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 { createInventoryItemWorkflow } from "../workflows/create-inventory-item" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createInventoryItemWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createInventoryItemWorkflow(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). + +*** + + +# Order Module + +In this section of the documentation, you will find resources to learn more about the Order Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/index.html.md) to learn how to manage orders using the dashboard. + +Medusa has order related features available out-of-the-box through the Order 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 Order Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Order Features + +- [Order Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/concepts/index.html.md): Store and manage your orders to retrieve, create, cancel, and perform other operations. +- Draft Orders: Allow merchants to create orders on behalf of their customers as draft orders that later are transformed to regular orders. +- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/promotion-adjustments/index.html.md): Apply promotions or discounts to the order's 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/order/tax-lines/index.html.md): Apply tax lines to an order's line items and shipping methods. +- [Returns](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md), [Edits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md), [Exchanges](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md), and [Claims](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md): Make [changes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to an order to edit, return, or exchange its items, with [version-based control](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) over the order's timeline. + +*** + +## How to Use the Order 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. + +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-draft-order.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createDraftOrderStep = createStep( + "create-order", + async ({}, { container }) => { + const orderModuleService = container.resolve(Modules.ORDER) + + const draftOrder = await orderModuleService.createOrders({ + currency_code: "usd", + items: [ + { + title: "Shirt", + quantity: 1, + unit_price: 3000, + }, + ], + shipping_methods: [ + { + name: "Express shipping", + amount: 3000, + }, + ], status: "draft", }) @@ -15746,27 +15885,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. @@ -15774,7 +15913,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, @@ -15783,46 +15922,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, }) } ) @@ -15837,13 +15965,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) @@ -15857,13 +15985,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) @@ -15878,12 +16006,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) @@ -15899,26 +16027,40 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +## Configure Payment Module -# Product 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 Product Module and how to use it in your application. +*** -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/products/index.html.md) to learn how to manage products using the dashboard. +## Providers -Medusa has product related features available out-of-the-box through the Product 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 Product 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). -## Product Features +## Pricing Features -- [Products Management](https://docs.medusajs.com/references/product/models/Product/index.html.md): Store and manage products. Products have custom options, such as color or size, and each variant in the product sets the value for these options. -- [Product Organization](https://docs.medusajs.com/references/product/models/index.html.md): The Product Module provides different data models used to organize products, including categories, collections, tags, and more. -- [Bundled and Multi-Part Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. +- [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 Product 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. @@ -15926,7 +16068,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-product.ts" highlights={highlights} +```ts title="src/workflows/create-price-set.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -15935,48 +16077,46 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createProductStep = createStep( - "create-product", +const createPriceSetStep = createStep( + "create-price-set", async ({}, { container }) => { - const productService = container.resolve(Modules.PRODUCT) + const pricingModuleService = container.resolve(Modules.PRICING) - const product = await productService.createProducts({ - title: "Medusa Shirt", - options: [ + const priceSet = await pricingModuleService.createPriceSets({ + prices: [ { - title: "Color", - values: ["Black", "White"], + amount: 500, + currency_code: "USD", }, - ], - variants: [ { - title: "Black Shirt", - options: { - Color: "Black", - }, + amount: 400, + currency_code: "EUR", + min_quantity: 0, + max_quantity: 4, + rules: {}, }, ], }) - return new StepResponse({ product }, product.id) + return new StepResponse({ priceSet }, priceSet.id) }, - async (productId, { container }) => { - if (!productId) { + async (priceSetId, { container }) => { + if (!priceSetId) { return } - const productService = container.resolve(Modules.PRODUCT) + const pricingModuleService = container.resolve(Modules.PRICING) - await productService.deleteProducts([productId]) + await pricingModuleService.deletePriceSets([priceSetId]) } ) -export const createProductWorkflow = createWorkflow( - "create-product", +export const createPriceSetWorkflow = createWorkflow( + "create-price-set", () => { - const { product } = createProductStep() + const { priceSet } = createPriceSetStep() return new WorkflowResponse({ - product, + priceSet, }) } ) @@ -15991,13 +16131,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createProductWorkflow } from "../../workflows/create-product" +import { createPriceSetWorkflow } from "../../workflows/create-price-set" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createProductWorkflow(req.scope) + const { result } = await createPriceSetWorkflow(req.scope) .run() res.send(result) @@ -16011,13 +16151,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createProductWorkflow } from "../workflows/create-product" +import { createPriceSetWorkflow } from "../workflows/create-price-set" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createProductWorkflow(container) + const { result } = await createPriceSetWorkflow(container) .run() console.log(result) @@ -16032,12 +16172,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createProductWorkflow } from "../workflows/create-product" +import { createPriceSetWorkflow } from "../workflows/create-price-set" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createProductWorkflow(container) + const { result } = await createPriceSetWorkflow(container) .run() console.log(result) @@ -16054,27 +16194,25 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Payment Module +# Product Module -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. +In this section of the documentation, you will find resources to learn more about the Product 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. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/products/index.html.md) to learn how to manage products using the dashboard. -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 has product related features available out-of-the-box through the Product 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 Product 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 +## Product 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. +- [Products Management](https://docs.medusajs.com/references/product/models/Product/index.html.md): Store and manage products. Products have custom options, such as color or size, and each variant in the product sets the value for these options. +- [Product Organization](https://docs.medusajs.com/references/product/models/index.html.md): The Product Module provides different data models used to organize products, including categories, collections, tags, and more. +- [Bundled and Multi-Part Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. *** -## How to Use the Payment Module +## How to Use the Product 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. @@ -16082,7 +16220,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-product.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -16091,35 +16229,48 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createPaymentCollectionStep = createStep( - "create-payment-collection", +const createProductStep = createStep( + "create-product", async ({}, { container }) => { - const paymentModuleService = container.resolve(Modules.PAYMENT) + const productService = container.resolve(Modules.PRODUCT) - const paymentCollection = await paymentModuleService.createPaymentCollections({ - currency_code: "usd", - amount: 5000, + const product = await productService.createProducts({ + title: "Medusa Shirt", + options: [ + { + title: "Color", + values: ["Black", "White"], + }, + ], + variants: [ + { + title: "Black Shirt", + options: { + Color: "Black", + }, + }, + ], }) - return new StepResponse({ paymentCollection }, paymentCollection.id) + return new StepResponse({ product }, product.id) }, - async (paymentCollectionId, { container }) => { - if (!paymentCollectionId) { + async (productId, { container }) => { + if (!productId) { return } - const paymentModuleService = container.resolve(Modules.PAYMENT) + const productService = container.resolve(Modules.PRODUCT) - await paymentModuleService.deletePaymentCollections([paymentCollectionId]) + await productService.deleteProducts([productId]) } ) -export const createPaymentCollectionWorkflow = createWorkflow( - "create-payment-collection", +export const createProductWorkflow = createWorkflow( + "create-product", () => { - const { paymentCollection } = createPaymentCollectionStep() + const { product } = createProductStep() return new WorkflowResponse({ - paymentCollection, + product, }) } ) @@ -16134,13 +16285,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection" +import { createProductWorkflow } from "../../workflows/create-product" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createPaymentCollectionWorkflow(req.scope) + const { result } = await createProductWorkflow(req.scope) .run() res.send(result) @@ -16154,13 +16305,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" +import { createProductWorkflow } from "../workflows/create-product" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createPaymentCollectionWorkflow(container) + const { result } = await createProductWorkflow(container) .run() console.log(result) @@ -16175,12 +16326,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 { createProductWorkflow } from "../workflows/create-product" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createPaymentCollectionWorkflow(container) + const { result } = await createProductWorkflow(container) .run() console.log(result) @@ -16196,18 +16347,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 @@ -16938,25 +17077,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Tax Module +# User 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. +In this section of the documentation, you will find resources to learn more about the User 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. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users 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. +Medusa has user related features available out-of-the-box through the User 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 User 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 +## User 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. +- [User Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows/index.html.md): Store and manage users in your store. +- [Invite Users](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows#invite-users/index.html.md): Invite users to join your store and manage those invites. *** -## How to Use Tax Module's Service +## How to Use User 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. @@ -16964,7 +17102,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-tax-region.ts" highlights={highlights} +```ts title="src/workflows/create-user.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -16973,33 +17111,37 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createTaxRegionStep = createStep( - "create-tax-region", +const createUserStep = createStep( + "create-user", async ({}, { container }) => { - const taxModuleService = container.resolve(Modules.TAX) + const userModuleService = container.resolve(Modules.USER) - const taxRegion = await taxModuleService.createTaxRegions({ - country_code: "us", + const user = await userModuleService.createUsers({ + email: "user@example.com", + first_name: "John", + last_name: "Smith", }) - return new StepResponse({ taxRegion }, taxRegion.id) + return new StepResponse({ user }, user.id) }, - async (taxRegionId, { container }) => { - if (!taxRegionId) { + async (userId, { container }) => { + if (!userId) { return } - const taxModuleService = container.resolve(Modules.TAX) + const userModuleService = container.resolve(Modules.USER) - await taxModuleService.deleteTaxRegions([taxRegionId]) + await userModuleService.deleteUsers([userId]) } ) -export const createTaxRegionWorkflow = createWorkflow( - "create-tax-region", +export const createUserWorkflow = createWorkflow( + "create-user", () => { - const { taxRegion } = createTaxRegionStep() + const { user } = createUserStep() - return new WorkflowResponse({ taxRegion }) + return new WorkflowResponse({ + user, + }) } ) ``` @@ -17013,13 +17155,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createTaxRegionWorkflow } from "../../workflows/create-tax-region" +import { createUserWorkflow } from "../../workflows/create-user" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createTaxRegionWorkflow(req.scope) + const { result } = await createUserWorkflow(req.scope) .run() res.send(result) @@ -17033,13 +17175,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createTaxRegionWorkflow } from "../workflows/create-tax-region" +import { createUserWorkflow } from "../workflows/create-user" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createTaxRegionWorkflow(container) + const { result } = await createUserWorkflow(container) .run() console.log(result) @@ -17054,12 +17196,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createTaxRegionWorkflow } from "../workflows/create-tax-region" +import { createUserWorkflow } from "../workflows/create-user" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createTaxRegionWorkflow(container) + const { result } = await createUserWorkflow(container) .run() console.log(result) @@ -17075,31 +17217,32 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -## Configure Tax Module +## Configure User Module -The Tax Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) for details on the module's options. +The User Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/module-options/index.html.md) for details on the module's options. *** -# User Module +# Tax Module -In this section of the documentation, you will find resources to learn more about the User Module and how to use it in your application. +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/users/index.html.md) to learn how to manage users using the dashboard. +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 user related features available out-of-the-box through the User 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 User Module. +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). -## User Features +## Tax Features -- [User Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows/index.html.md): Store and manage users in your store. -- [Invite Users](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows#invite-users/index.html.md): Invite users to join your store and manage those invites. +- [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 User Module's Service +## 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. @@ -17107,7 +17250,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-user.ts" highlights={highlights} +```ts title="src/workflows/create-tax-region.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -17116,37 +17259,33 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createUserStep = createStep( - "create-user", +const createTaxRegionStep = createStep( + "create-tax-region", async ({}, { container }) => { - const userModuleService = container.resolve(Modules.USER) + const taxModuleService = container.resolve(Modules.TAX) - const user = await userModuleService.createUsers({ - email: "user@example.com", - first_name: "John", - last_name: "Smith", + const taxRegion = await taxModuleService.createTaxRegions({ + country_code: "us", }) - return new StepResponse({ user }, user.id) + return new StepResponse({ taxRegion }, taxRegion.id) }, - async (userId, { container }) => { - if (!userId) { + async (taxRegionId, { container }) => { + if (!taxRegionId) { return } - const userModuleService = container.resolve(Modules.USER) + const taxModuleService = container.resolve(Modules.TAX) - await userModuleService.deleteUsers([userId]) + await taxModuleService.deleteTaxRegions([taxRegionId]) } ) -export const createUserWorkflow = createWorkflow( - "create-user", +export const createTaxRegionWorkflow = createWorkflow( + "create-tax-region", () => { - const { user } = createUserStep() + const { taxRegion } = createTaxRegionStep() - return new WorkflowResponse({ - user, - }) + return new WorkflowResponse({ taxRegion }) } ) ``` @@ -17160,13 +17299,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createUserWorkflow } from "../../workflows/create-user" +import { createTaxRegionWorkflow } from "../../workflows/create-tax-region" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createUserWorkflow(req.scope) + const { result } = await createTaxRegionWorkflow(req.scope) .run() res.send(result) @@ -17180,13 +17319,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createUserWorkflow } from "../workflows/create-user" +import { createTaxRegionWorkflow } from "../workflows/create-tax-region" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createUserWorkflow(container) + const { result } = await createTaxRegionWorkflow(container) .run() console.log(result) @@ -17201,12 +17340,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createUserWorkflow } from "../workflows/create-user" +import { createTaxRegionWorkflow } from "../workflows/create-tax-region" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createUserWorkflow(container) + const { result } = await createTaxRegionWorkflow(container) .run() console.log(result) @@ -17222,703 +17361,1105 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -## Configure User Module +## Configure Tax Module -The User Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/module-options/index.html.md) for details on the module's options. +The Tax Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) for details on the module's options. *** -# Authentication Flows with the Auth Main Service - -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. - -## Authentication Methods +# API Key Concepts -### Register +In this document, you’ll learn about the different types of API keys, their expiration and verification. -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. +## API Key Types -For example: +There are two types of API keys: -```ts -const data = await authModuleService.register( - "emailpass", - // passed to auth provider - { - // ... - } -) -``` +- `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. -This method calls the `register` method of the provider specified in the first parameter and returns its data. +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). -### Authenticate +*** -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: +## API Key Expiration -```ts -const data = await authModuleService.authenticate( - "emailpass", - // passed to auth provider - { - // ... - } -) -``` +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). -This method calls the `authenticate` method of the provider specified in the first parameter and returns its data. +The associated token is no longer usable or verifiable. *** -## Auth Flow 1: Basic Authentication +## Token Verification -The basic authentication flow requires first using the `register` method, then the `authenticate` method: +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. -```ts -const { success, authIdentity, error } = await authModuleService.register( - "emailpass", - // passed to auth provider - { - // ... - } -) -if (error) { - // registration failed - // TODO return an error - return -} +# Links between API Key Module and Other Modules -// later (can be another route for log-in) -const { success, authIdentity, location } = await authModuleService.authenticate( - "emailpass", - // passed to auth provider - { - // ... - } -) +This document showcases the module links defined between the API Key Module and other commerce modules. -if (success && !location) { - // user is authenticated -} -``` +## Summary -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 API Key Module has the following links to other modules: -The next section explains the flow if `location` is set. +- [`ApiKey` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). -Check out the [AuthIdentity](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) reference for the received properties in `authIdentity`. +*** - +## Sales Channel Module -### Auth Identity with Same Identifier +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. -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. + -There are two ways to handle this: +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. -- 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. +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 -## Auth Flow 2: Third-Party Service Authentication +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`: -The third-party service authentication method requires using the `authenticate` method first: +### query.graph ```ts -const { success, authIdentity, location } = await authModuleService.authenticate( - "google", - // passed to auth provider - { - // ... - } -) - -if (location) { - // return the location for the front-end to redirect to -} - -if (!success) { - // authentication failed -} +const { data: apiKeys } = await query.graph({ + entity: "api_key", + fields: [ + "sales_channels.*", + ], +}) -// authentication successful +// apiKeys.sales_channels ``` -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. - -For example, when using the `google` provider, the `location` is the URL that the user is navigated to login. - - - -### Overriding Callback URL - -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. +### useQueryGraphStep ```ts -const { success, authIdentity, location } = await authModuleService.authenticate( - "google", - // passed to auth provider - { - // ... - callback_url: "example.com", - } -) -``` - -### validateCallback - -Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service. - -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). +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -The method calls the specified provider’s `validateCallback` method passing it the authentication details it received in the second parameter: +// ... -```ts -const { success, authIdentity } = await authModuleService.validateCallback( - "google", - // passed to auth provider - { - // request data, such as - url, - headers, - query, - body, - protocol, - } -) +const { data: apiKeys } = useQueryGraphStep({ + entity: "api_key", + fields: [ + "sales_channels.*", + ], +}) -if (success) { - // authentication succeeded -} +// apiKeys.sales_channels ``` -For providers like Google, the `query` object contains the query parameters from the original callback URL, such as the `code` and `state` parameters. +### Manage with Link -If the returned `success` property is `true`, the authentication with the third-party provider was successful. +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" -## Reset Password +// ... -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. +await link.create({ + [Modules.API_KEY]: { + api_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` -For example: +### createRemoteLinkStep ```ts -const { success } = await authModuleService.updateProvider( - "emailpass", - // passed to the auth provider - { - entity_id: "user@example.com", - password: "supersecret", - } -) +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -if (success) { - // password reset successfully -} +// ... + +createRemoteLinkStep({ + [Modules.API_KEY]: { + api_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) ``` -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. +# Cart Concepts -If the returned `success` property is `true`, the password has reset successfully. +In this document, you’ll get an overview of the main concepts of a cart. +## Shipping and Billing Addresses -# Auth Providers +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 this document, you’ll learn how the Auth Module handles authentication using providers. + -## What's an Auth Module Provider? +*** -An auth module provider handles authenticating customers and users, either using custom logic or by integrating a third-party service. +## Line Items -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 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. -### Auth Providers List +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. -- [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) +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). *** -## Configure Allowed Auth Providers of Actor Types +## Shipping Methods -By default, users of all actor types can authenticate with all installed auth module providers. +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. -To restrict the auth providers used for actor types, use the [authMethodsPerActor option](https://docs.medusajs.com/references/medusa-config#http-authMethodsPerActor-1-3/index.html.md) in Medusa's configurations: +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. -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - http: { - authMethodsPerActor: { - user: ["google"], - customer: ["emailpass"], - }, - // ... - }, - // ... - }, -}) -``` +### data Property -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. +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. -## How to Create an Auth Module Provider +The `data` property is an object used to store custom data relevant later for fulfillment. -Refer to [this guide](https://docs.medusajs.com/references/auth/provider/index.html.md) to learn how to create an auth module provider. +# Links between Cart Module and Other Modules -# Auth Identity and Actor Types +This document showcases the module links defined between the Cart Module and other commerce modules. -In this document, you’ll learn about concepts related to identity and actors in the Auth Module. +## Summary -## What is an Auth Identity? +The Cart Module has the following links to other modules: -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`. +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -Then, when the user logs-in in the future with the same authentication provider, the associated auth identity is used to validate their credentials. +- [`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). *** -## Actor Types - -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). +## Customer Module -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. +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. -For example, an auth identity of a customer has the following `app_metadata` property: +### Retrieve with Query -```json -{ - "app_metadata": { - "customer_id": "cus_123" - } -} -``` +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`: -The ID of the user is stored in the key `{actor_type}_id` of the `app_metadata` property. +### query.graph -*** +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "customer.*", + ], +}) -## Protect Routes by Actor Type +// carts.order +``` -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. +### useQueryGraphStep -For example: +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -```ts title="src/api/middlewares.ts" highlights={highlights} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" +// ... -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom/admin*", - middlewares: [ - authenticate("user", ["session", "bearer", "api-key"]), - ], - }, +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "customer.*", ], }) -``` -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`. +// carts.order +``` *** -## Custom Actor Types +## Order Module -You can define custom actor types that allows a custom user, managed by your custom module, to authenticate into Medusa. +The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management features. -For example, if you have a custom module with a `Manager` data model, you can authenticate managers with the `manager` actor type. +Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. -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). + +### Retrieve with Query -# How to Use Authentication Routes +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`: -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. +### query.graph -These routes are added by Medusa's HTTP layer, not the Auth Module. +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "order.*", + ], +}) -## Types of Authentication Flows +// carts.order +``` -### 1. Basic Authentication Flow +### useQueryGraphStep -This authentication flow doesn't require validation with third-party services. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -[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: +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "order.*", + ], +}) - +// carts.order +``` -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). +### Manage with Link -After registration, you only use the [Auth Route](#login-route) for subsequent authentication. +To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -To handle errors related to existing identities, refer to [this section](#handling-existing-identities). +### link.create -### 2. Third-Party Service Authenticate Flow +```ts +import { Modules } from "@medusajs/framework/utils" -This authentication flow authenticates the user with a third-party service, such as Google. +// ... -[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). +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.ORDER]: { + order_id: "order_123", + }, +}) +``` -It requires the following steps: +### createRemoteLinkStep - +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -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. +// ... -*** +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.ORDER]: { + order_id: "order_123", + }, +}) +``` -## Register Route +*** -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. +## Payment Module -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com" - // ... -}' -``` +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. -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. +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. -For example, if you're registering a customer, you: + -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). +### Retrieve with Query -### Path Parameters +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`: -Its path parameters are: +### query.graph -- `{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`. +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "payment_collection.*", + ], +}) -### Request Body Parameters +// carts.payment_collection +``` -This route accepts in the request body the data that the specified authentication provider requires to handle authentication. +### useQueryGraphStep -For example, the EmailPass provider requires an `email` and `password` fields in the request body. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -### Response Fields +// ... -If the authentication is successful, you'll receive a `token` field in the response body object: +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "payment_collection.*", + ], +}) -```json -{ - "token": "..." -} +// carts.payment_collection ``` -Use that token in the header of subsequent requests to send authenticated requests. +### Manage with Link -### Handling Existing Identities +To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -An auth identity with the same email may already exist in Medusa. This can happen if: +### link.create -- 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. +```ts +import { Modules } from "@medusajs/framework/utils" -In these scenarios, the Register Route will return an error instead of a token: +// ... -```json -{ - "type": "unauthorized", - "message": "Identity with email already exists" -} +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) ``` -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. +### createRemoteLinkStep -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: +```ts +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -```json -{ - "type": "unauthorized", - "message": "Invalid email or password" -} -``` +// ... -You can show that error message to the customer. +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` *** -## Login Route - -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. - -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers} --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com" - // ... -}' -``` +## Product Module -For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`. +Medusa defines read-only links between: -### Path Parameters +- 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. -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 variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: -### Request Body Parameters +To retrieve the product, pass `product.*` in `fields`. -This route accepts in the request body the data that the specified authentication provider requires to handle authentication. +### query.graph -For example, the EmailPass provider requires an `email` and `password` fields in the request body. +```ts +const { data: lineItems } = await query.graph({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) -#### Overriding Callback URL +// lineItems.variant +``` -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. +### useQueryGraphStep -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. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -### Response Fields +// ... -If the authentication is successful, you'll receive a `token` field in the response body object: +const { data: lineItems } = useQueryGraphStep({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) -```json -{ - "token": "..." -} +// lineItems.variant ``` -Use that token in the header of subsequent requests to send authenticated requests. - -If the authentication requires more action with a third-party service, you'll receive a `location` property: +*** -```json -{ - "location": "https://..." -} -``` +## Promotion Module -Redirect to that URL in the frontend to continue the authentication process with the third-party service. +The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features. -[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). +Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart. -*** + -## Validate Callback Route +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. -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. +### Retrieve with Query -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/callback?code=123&state=456 -``` +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`: -Refer to the [third-party authentication flow](#2-third-party-service-authenticate-flow) section to see how this route fits into the authentication flow. +To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. -### Path Parameters +### query.graph -Its path parameters are: +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "promotions.*", + ], +}) -- `{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`. +// carts.promotions +``` -### Query Parameters +### useQueryGraphStep -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. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -### Response Fields +// ... -If the authentication is successful, you'll receive a `token` field in the response body object: +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "promotions.*", + ], +}) -```json -{ - "token": "..." -} +// carts.promotions ``` -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). +### 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): -## Refresh Token Route +### link.create -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 { Modules } from "@medusajs/framework/utils" -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}' +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) ``` -### Response Fields +### createRemoteLinkStep -If the token was refreshed successfully, you'll receive a `token` field in the response body object: +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -```json -{ - "token": "..." -} -``` +// ... -Use that token in the header of subsequent requests to send authenticated requests. +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` *** -## Reset Password Routes +## Region Module -To reset a user's password: +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. -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. +### Retrieve with Query -[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) +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`: -### Generate Reset Password Token Route +### query.graph -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. +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "region.*", + ], +}) -```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" -}' +// carts.region ``` -This API route is useful for providers like `emailpass` that store a user's password and use it for 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: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "region.*", + ], +}) -#### Request Body Parameters +// carts.region +``` -This route accepts in the request body an object having the following property: +*** -- `identifier`: The user's identifier in the specified auth provider. For example, for the `emailpass` auth provider, you pass the user's email. +## Sales Channel Module -#### Response Fields +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. -If the authentication is successful, the request returns a `201` response code. +### Retrieve with Query -### Reset Password Route +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`: -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. +### query.graph -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/update?token=123 --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com", - "password": "supersecret" -}' +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) + +// carts.sales_channel ``` -This API route is useful for providers like `emailpass` that store a user's password and use it for logging them in. +### 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: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) -#### Query Parameters +// carts.sales_channel +``` -The route accepts a `token` query parameter, which is the token generated using the [Generate Reset Password Token route](#generate-reset-password-token-route). -### Request Body Parameters +# Promotions Adjustments in Carts -This route accepts in the request body an object that has the data necessary for the provider to update the user's password. +In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. -For the `emailpass` provider, you must pass the following properties: +## What are Adjustment Lines? -- `email`: The user's email. -- `password`: The new password. +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. -### Response Fields +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. -If the authentication is successful, the request returns an object with a `success` property set to `true`: + -```json -{ - "success": "true" -} -``` +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. +*** -# How to Create an Actor Type +## Discountable Option -In this document, learn how to create an actor type and authenticate its associated data model. +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. -## 0. Create Module with Data Model +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. -Before creating an actor type, you must have a module with a data model representing the actor type. +*** -Learn how to create a module in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). +## Promotion Actions -The rest of this guide uses this `Manager` data model as an example: +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. -```ts title="src/modules/manager/models/manager.ts" -import { model } from "@medusajs/framework/utils" +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). -const Manager = model.define("manager", { - id: model.id().primaryKey(), - firstName: model.text(), - lastName: model.text(), - email: model.text(), +For example: + +```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", + ], }) -export default 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, + }) + } +}) -*** +// 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, + }) + } +}) -## 1. Create Workflow +// compute actions +const actions = await promotionModuleService.computeActions( + ["promo_123"], + { + items: lineItemAdjustments, + shipping_methods: shippingMethodAdjustments, + } +) +``` -Start by creating a workflow that does two things: +The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. + +Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments. + +```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + AddItemAdjustmentAction, + AddShippingMethodAdjustment, + // ... +} from "@medusajs/framework/types" + +// ... + +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[] +) +``` + + +# Tax Lines in Cart Module + +In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. + +## 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. + + + +*** + +## 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 them 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 taxes perspective. + + + +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`. + +*** + +## Retrieve Tax Lines + +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. + +```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", + }, + } +) +``` + +Then, use the returned tax lines to set the line items and shipping methods’ tax lines: + +```ts +// set line item tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "line_item_id" in line) +) + +// set shipping method tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "shipping_line_id" in line) +) +``` + + +# Auth Identity and Actor Types + +In this document, you’ll learn about concepts related to identity and actors in the Auth Module. + +## What is an Auth Identity? + +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`. + +Then, when the user logs-in in the future with the same authentication provider, the associated auth identity is used to validate their credentials. + +*** + +## Actor Types + +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). + +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. + +For example, an auth identity of a customer has the following `app_metadata` property: + +```json +{ + "app_metadata": { + "customer_id": "cus_123" + } +} +``` + +The ID of the user is stored in the key `{actor_type}_id` of the `app_metadata` property. + +*** + +## Protect Routes by Actor Type + +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. + +For example: + +```ts title="src/api/middlewares.ts" highlights={highlights} +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/admin*", + middlewares: [ + authenticate("user", ["session", "bearer", "api-key"]), + ], + }, + ], +}) +``` + +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`. + +*** + +## Custom Actor Types + +You can define custom actor types that allows a custom user, managed by your custom module, to authenticate into Medusa. + +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). + + +# Authentication Flows with the Auth Main Service + +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. + +## Authentication Methods + +### Register + +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. + +For example: + +```ts +const data = await authModuleService.register( + "emailpass", + // passed to auth provider + { + // ... + } +) +``` + +This method calls the `register` method of the provider specified in the first parameter and returns its data. + +### Authenticate + +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: + +```ts +const data = await authModuleService.authenticate( + "emailpass", + // passed to auth provider + { + // ... + } +) +``` + +This method calls the `authenticate` method of the provider specified in the first parameter and returns its data. + +*** + +## Auth Flow 1: Basic Authentication + +The basic authentication flow requires first using the `register` method, then the `authenticate` method: + +```ts +const { success, authIdentity, error } = await authModuleService.register( + "emailpass", + // passed to auth provider + { + // ... + } +) + +if (error) { + // registration failed + // TODO return an error + return +} + +// later (can be another route for log-in) +const { success, authIdentity, location } = await authModuleService.authenticate( + "emailpass", + // passed to auth provider + { + // ... + } +) + +if (success && !location) { + // user is authenticated +} +``` + +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. + +Check out the [AuthIdentity](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) reference for the received properties in `authIdentity`. + + + +### Auth Identity with Same Identifier + +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. + +There are two ways to handle this: + +- 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. + +*** + +## Auth Flow 2: Third-Party Service Authentication + +The third-party service authentication method requires using the `authenticate` method first: + +```ts +const { success, authIdentity, location } = await authModuleService.authenticate( + "google", + // passed to auth provider + { + // ... + } +) + +if (location) { + // return the location for the front-end to redirect to +} + +if (!success) { + // authentication failed +} + +// authentication successful +``` + +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. + +For example, when using the `google` provider, the `location` is the URL that the user is navigated to login. + + + +### Overriding Callback URL + +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", + } +) +``` + +### validateCallback + +Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service. + +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 +const { success, authIdentity } = await authModuleService.validateCallback( + "google", + // passed to auth provider + { + // request data, such as + url, + headers, + query, + body, + protocol, + } +) + +if (success) { + // authentication succeeded +} +``` + +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. + + + +*** + +## Reset Password + +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. + +For example: + +```ts +const { success } = await authModuleService.updateProvider( + "emailpass", + // passed to the auth provider + { + entity_id: "user@example.com", + password: "supersecret", + } +) + +if (success) { + // password reset successfully +} +``` + +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. + + +# How to Create an Actor Type + +In this document, learn how to create an actor type and authenticate its associated data model. + +## 0. Create Module with Data Model + +Before creating an actor type, you must have a module with a data model representing the actor type. + +Learn how to create a module in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). + +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" + +const Manager = model.define("manager", { + id: model.id().primaryKey(), + firstName: model.text(), + lastName: model.text(), + email: model.text(), +}) + +export default Manager +``` + +*** + +## 1. Create Workflow + +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. @@ -18282,89 +18823,504 @@ In the workflow, you: You can use this workflow when deleting a manager, such as in an API route. -# Auth Module Options +# How to Use Authentication Routes -In this document, you'll learn about the options of the Auth Module. +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. -## providers +These routes are added by Medusa's HTTP layer, not the Auth Module. -The `providers` option is an array of auth module providers. +## Types of Authentication Flows -When the Medusa application starts, these providers are registered and can be used to handle authentication. +### 1. Basic Authentication Flow -By default, the `emailpass` provider is registered to authenticate customers and admin users. +This authentication flow doesn't require validation with third-party services. -For example: +[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). -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" +The steps are: -// ... + -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... - }, - }, - ], - }, - }, - ], -}) -``` +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). -The `providers` option is an array of objects that accept the following properties: +After registration, you only use the [Auth Route](#login-route) for subsequent authentication. -- `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 handle errors related to existing identities, refer to [this section](#handling-existing-identities). -*** +### 2. Third-Party Service Authenticate Flow -## Auth CORS +This authentication flow authenticates the user with a third-party service, such as Google. -The Medusa application's authentication API routes are defined under the `/auth` prefix that requires setting the `authCors` property of the `http` configuration. +[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). -By default, the Medusa application you created will have an `AUTH_CORS` environment variable, which is used as the value of `authCors`. +It requires the following steps: -Refer to [Medusa's configuration guide](https://docs.medusajs.com/references/medusa-config#authCors/index.html.md) to learn more about the `authCors` configuration. + + +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. *** -## authMethodsPerActor Configuration +## Register Route -The Medusa application's configuration accept an `authMethodsPerActor` configuration which restricts the allowed auth providers used with an actor type. +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. -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). +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "Whitney_Schultz@gmail.com" + // ... +}' +``` +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. -# How to Handle Password Reset Token Event +For example, if you're registering a customer, you: -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). +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). -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. +### Path Parameters -You'll create a subscriber that listens to the event. When the event is emitted, the subscriber sends an email notification to the user. +Its path parameters are: -### Prerequisites +- `{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`. -- [A notification provider module, such as SendGrid](https://docs.medusajs.com/architectural-modules/notification/sendgrid/index.html.md) +### Request Body Parameters -## 1. Create Subscriber +This route accepts in the request body the data that the specified authentication provider requires to handle authentication. -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. +For example, the EmailPass provider requires an `email` and `password` fields in the request body. -Create the file `src/subscribers/handle-reset.ts` with the following content: +### Response Fields + +If the authentication is successful, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." +} +``` + +Use that token in the header of subsequent requests to send authenticated requests. + +### Handling Existing Identities + +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. + +In these scenarios, the Register Route will return an error instead of a token: + +```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. + +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: + +```json +{ + "type": "unauthorized", + "message": "Invalid email or password" +} +``` + +You can show that error message to the customer. + +*** + +## Login Route + +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. + +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers} +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "Whitney_Schultz@gmail.com" + // ... +}' +``` + +For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`. + +### 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, `emailpass`. + +### Request Body Parameters + +This route accepts in the request body the data that the specified authentication provider requires to handle authentication. + +For example, the EmailPass provider requires an `email` and `password` fields in the request body. + +#### 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. + +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. + +### Response Fields + +If the authentication is successful, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." +} +``` + +Use that token in the header of subsequent requests to send authenticated requests. + +If the authentication requires more action with a third-party service, you'll receive a `location` property: + +```json +{ + "location": "https://..." +} +``` + +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). + +*** + +## 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. + +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/callback?code=123&state=456 +``` + +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`. + +### Query Parameters + +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. + +### Response Fields + +If the authentication is successful, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." +} +``` + +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 + +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. + +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}' +``` + +### Response Fields + +If the token was refreshed successfully, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." +} +``` + +Use that token in the header of subsequent requests to send authenticated requests. + +*** + +## Reset Password Routes + +To reset a user's password: + +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. + +[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) + +### Generate Reset Password Token Route + +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" +}' +``` + +This API route is useful for providers like `emailpass` that store a user's password and use it for authentication. + +#### 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, `emailpass`. + +#### Request Body Parameters + +This route accepts in the request body an object having the following property: + +- `identifier`: The user's identifier in the specified auth provider. For example, for the `emailpass` auth provider, you pass the user's email. + +#### Response Fields + +If the authentication is successful, the request returns a `201` response code. + +### Reset Password Route + +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" +}' +``` + +This API route is useful for providers like `emailpass` that store a user's password and use it for logging them in. + +#### 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, `emailpass`. + +#### Pass Token in Authorization Header + +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. + +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. + +### 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. + +For the `emailpass` provider, you must pass the following properties: + +- `email`: The user's email. +- `password`: The new password. + +### Response Fields + +If the authentication is successful, the request returns an object with a `success` property set to `true`: + +```json +{ + "success": "true" +} +``` + + +# Auth Providers + +In this document, you’ll learn how the Auth Module handles authentication using providers. + +## What's an Auth Module Provider? + +An auth module provider handles authenticating customers and users, either using custom logic or by integrating a third-party service. + +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. + +### Auth Providers List + +- [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) + +*** + +## Configure Allowed Auth Providers of Actor Types + +By default, users of all actor types can authenticate with all installed auth module providers. + +To restrict the auth providers used for actor types, use the [authMethodsPerActor option](https://docs.medusajs.com/references/medusa-config#http-authMethodsPerActor-1-3/index.html.md) in Medusa's configurations: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + http: { + authMethodsPerActor: { + user: ["google"], + customer: ["emailpass"], + }, + // ... + }, + // ... + }, +}) +``` + +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. + +*** + +## How to Create an Auth Module Provider + +Refer to [this guide](https://docs.medusajs.com/references/auth/provider/index.html.md) to learn how to create an auth module provider. + + +# Auth Module Options + +In this document, you'll learn about the options of the Auth Module. + +## providers + +The `providers` option is an array of auth module providers. + +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. + +For example: + +```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: [ + { + resolve: "@medusajs/medusa/auth-emailpass", + id: "emailpass", + options: { + // provider options... + }, + }, + ], + }, + }, + ], +}) +``` + +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. + +*** + +## Auth CORS + +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`. + +Refer to [Medusa's configuration guide](https://docs.medusajs.com/references/medusa-config#authCors/index.html.md) to learn more about the `authCors` configuration. + +*** + +## authMethodsPerActor Configuration + +The Medusa application's configuration accept an `authMethodsPerActor` configuration which restricts the allowed auth providers used with an actor type. + +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). + + +# Customer Accounts + +In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. + +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. + +## `has_account` Property + +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. + +When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. + +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`. + +*** + +## Email Uniqueness + +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. + +So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. + + +# How to Handle Password Reset Token Event + +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. + +You'll create a subscriber that listens to the event. When the event is emitted, the subscriber sends an email notification to the user. + +### Prerequisites + +- [A notification provider module, such as SendGrid](https://docs.medusajs.com/architectural-modules/notification/sendgrid/index.html.md) + +## 1. Create Subscriber + +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. + +Create the file `src/subscribers/handle-reset.ts` with the following content: ```ts title="src/subscribers/handle-reset.ts" highlights={highlights} collapsibleLines="1-6" expandMoreLabel="Show Imports" import { @@ -18387,7 +19343,7 @@ export default async function resetPasswordTokenHandler({ const urlPrefix = actor_type === "customer" ? "https://storefront.com" : - "https://admin.com" + "https://admin.com/app" await notificationModuleService.createNotifications({ to: email, @@ -18462,71 +19418,43 @@ The page shows the user password fields to enter their new password, then submit - [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) -# 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: - -- `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. - -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). - -*** - -## API Key Expiration - -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 +# Links between Customer Module and Other Modules -This document showcases the module links defined between the API Key Module and other commerce modules. +This document showcases the module links defined between the Customer Module and other commerce modules. ## Summary -The API Key Module has the following links to other modules: - -- [`ApiKey` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). +The Customer 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. -## Sales Channel Module +- [`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). -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. +*** - +## Payment Module -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. +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. -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). +This link is available starting from Medusa `v2.5.0`. ### 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`: +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`: ### query.graph ```ts -const { data: apiKeys } = await query.graph({ - entity: "api_key", +const { data: customers } = await query.graph({ + entity: "customer", fields: [ - "sales_channels.*", + "account_holder.*", ], }) -// apiKeys.sales_channels +// customers.account_holder ``` ### useQueryGraphStep @@ -18536,19 +19464,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: apiKeys } = useQueryGraphStep({ - entity: "api_key", +const { data: customers } = useQueryGraphStep({ + entity: "customer", fields: [ - "sales_channels.*", + "account_holder.*", ], }) -// apiKeys.sales_channels +// customers.account_holder ``` ### 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): +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 @@ -18558,11 +19486,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.API_KEY]: { - api_key_id: "apk_123", + [Modules.CUSTOMER]: { + customer_id: "cus_123", }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", }, }) ``` @@ -18570,99 +19498,136 @@ await link.create({ ### 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.CUSTOMER]: { + customer_id: "cus_123", }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", }, }) ``` +*** -# Cart Concepts - -In this document, you’ll get an overview of the main concepts of a cart. - -## Shipping and Billing Addresses - -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). +## Cart Module - +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. -*** +### Retrieve with Query -## Line Items +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`: -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. +### query.graph -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. +```ts +const { data: customers } = await query.graph({ + entity: "customer", + fields: [ + "carts.*", + ], +}) -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). +// customers.carts +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: [ + "carts.*", + ], +}) + +// customers.carts +``` *** -## Shipping Methods +## Order 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. +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. -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. +### Retrieve with Query -### data Property +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`: -After an order is placed, you can use a third-party fulfillment provider to fulfill its shipments. +### query.graph -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. +```ts +const { data: customers } = await query.graph({ + entity: "customer", + fields: [ + "orders.*", + ], +}) -The `data` property is an object used to store custom data relevant later for fulfillment. +// customers.orders +``` +### useQueryGraphStep -# Links between Cart Module and Other Modules +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -This document showcases the module links defined between the Cart Module and other commerce modules. +// ... + +const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: [ + "orders.*", + ], +}) + +// customers.orders +``` + + +# Links between Currency Module and Other Modules + +This document showcases the module links defined between the Currency Module and other commerce modules. ## Summary -The Cart Module has the following links to other modules: +The Currency 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 \<> `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). +- [`Currency` data model of Store Module \<> `Currency` data model of Currency Module](#store-module). (Read-only). *** -## Customer Module +## Store 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. +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](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. ### 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`: +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`: ### query.graph ```ts -const { data: carts } = await query.graph({ - entity: "cart", +const { data: stores } = await query.graph({ + entity: "store", fields: [ - "customer.*", + "supported_currencies.currency.*", ], }) -// carts.order +// stores.supported_currencies ``` ### useQueryGraphStep @@ -18672,41 +19637,189 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: carts } = useQueryGraphStep({ - entity: "cart", +const { data: stores } = useQueryGraphStep({ + entity: "store", fields: [ - "customer.*", + "supported_currencies.currency.*", ], }) -// carts.order +// stores.supported_currencies +``` + + +# 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. + + + +*** + +## 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. + + + +*** + +## Fulfillment Label + +Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the `FulfillmentLabel` data model. + +*** + +## Fulfillment Status + +The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment: + +- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed. +- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped. +- `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered. + + +# 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 Concepts + +In this document, you’ll learn about some basic fulfillment concepts. + +## Fulfillment Set + +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. + +```ts +const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( + [ + { + name: "Shipping", + type: "shipping", + }, + { + name: "Pick-up", + type: "pick-up", + }, + ] +) ``` *** +## 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. + + + +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. + +*** + +## 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. + +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. + + +# Links between Fulfillment Module and Other Modules + +This document showcases the module links defined between the Fulfillment Module and other commerce modules. + +## Summary + +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). + +*** + ## 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. +The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management functionalities. -Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. +Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items. - + + +A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. + + ### Retrieve with Query -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`: +To retrieve the order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: + +To retrieve the return, pass `return.*` in `fields`. ### query.graph ```ts -const { data: carts } = await query.graph({ - entity: "cart", +const { data: fulfillments } = await query.graph({ + entity: "fulfillment", fields: [ "order.*", ], }) -// carts.order +// fulfillments.order ``` ### useQueryGraphStep @@ -18716,14 +19829,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: carts } = useQueryGraphStep({ - entity: "cart", +const { data: fulfillments } = useQueryGraphStep({ + entity: "fulfillment", fields: [ "order.*", ], }) -// carts.order +// fulfillments.order ``` ### Manage with Link @@ -18738,12 +19851,12 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, [Modules.ORDER]: { order_id: "order_123", }, + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", + }, }) ``` @@ -18756,40 +19869,40 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, [Modules.ORDER]: { order_id: "order_123", }, + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", + }, }) ``` *** -## Payment Module +## Pricing Module -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. +The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context. -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. +Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. - + ### 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`: +To retrieve the price set of a shipping option with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: ### query.graph ```ts -const { data: carts } = await query.graph({ - entity: "cart", +const { data: shippingOptions } = await query.graph({ + entity: "shipping_option", fields: [ - "payment_collection.*", + "price_set.*", ], }) -// carts.payment_collection +// shippingOptions.price_set ``` ### useQueryGraphStep @@ -18799,19 +19912,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: carts } = useQueryGraphStep({ - entity: "cart", +const { data: shippingOptions } = useQueryGraphStep({ + entity: "shipping_option", fields: [ - "payment_collection.*", + "price_set.*", ], }) -// carts.payment_collection +// shippingOptions.price_set ``` ### Manage with Link -To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -18821,11 +19934,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.CART]: { - cart_id: "cart_123", + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` @@ -18833,16 +19946,17 @@ await link.create({ ### createRemoteLinkStep ```ts +import { Modules } from "@medusajs/framework/utils" import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` @@ -18851,28 +19965,25 @@ createRemoteLinkStep({ ## Product Module -Medusa defines read-only links between: +Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile. -- 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. +This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). ### 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`: - -To retrieve the product, pass `product.*` in `fields`. +To retrieve the products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: ### query.graph ```ts -const { data: lineItems } = await query.graph({ - entity: "line_item", +const { data: shippingProfiles } = await query.graph({ + entity: "shipping_profile", fields: [ - "variant.*", + "products.*", ], }) -// lineItems.variant +// shippingProfiles.products ``` ### useQueryGraphStep @@ -18882,45 +19993,86 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: lineItems } = useQueryGraphStep({ - entity: "line_item", +const { data: shippingProfiles } = useQueryGraphStep({ + entity: "shipping_profile", fields: [ - "variant.*", + "products.*", ], }) -// lineItems.variant +// shippingProfiles.products +``` + +### Manage with Link + +To manage the shipping profile of a product, 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.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", + }, +}) ``` *** -## Promotion Module +## Stock Location Module -The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features. +The Stock Location Module provides features to manage stock locations in a store. -Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart. +Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location. - + -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. +Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. + + ### 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`: +To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` in `fields`: -To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. +To retrieve the stock location of a fulfillment provider, pass `locations.*` in `fields`. ### query.graph ```ts -const { data: carts } = await query.graph({ - entity: "cart", +const { data: fulfillmentSets } = await query.graph({ + entity: "fulfillment_set", fields: [ - "promotions.*", + "location.*", ], }) -// carts.promotions +// fulfillmentSets.location ``` ### useQueryGraphStep @@ -18930,19 +20082,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: carts } = useQueryGraphStep({ - entity: "cart", +const { data: fulfillmentSets } = useQueryGraphStep({ + entity: "fulfillment_set", fields: [ - "promotions.*", + "location.*", ], }) -// carts.promotions +// fulfillmentSets.location ``` ### 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): +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 @@ -18952,11 +20104,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.CART]: { - cart_id: "cart_123", + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", }, }) ``` @@ -18970,349 +20122,269 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_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 +# Fulfillment Module Options -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`: +In this document, you'll learn about the options of the Fulfillment Module. -### query.graph +## providers -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "region.*", - ], -}) +The `providers` option is an array of fulfillment module providers. -// carts.region -``` +When the Medusa application starts, these providers are registered and can be used to process fulfillments. -### useQueryGraphStep +For example: -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" // ... -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "region.*", +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/fulfillment", + options: { + providers: [ + { + resolve: `@medusajs/medusa/fulfillment-manual`, + id: "manual", + options: { + // provider options... + }, + }, + ], + }, + }, ], }) - -// carts.region ``` -*** +The `providers` option is an array of objects that accept the following properties: -## Sales Channel Module +- `resolve`: A string indicating either 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. -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 +# Shipping Option -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`: +In this document, you’ll learn about shipping options and their rules. -### query.graph +## What’s a Shipping Option? -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) +A shipping option is a way of shipping an item. Each fulfillment provider provides a set of shipping options. For example, a provider may provide a shipping option for express shipping and another for standard shipping. -// carts.sales_channel -``` +When the customer places their order, they choose a shipping option to be used to fulfill their items. -### useQueryGraphStep +A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +*** -// ... +## Service Zone Restrictions -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) +A shipping option is restricted by a service zone, limiting the locations a shipping option be used in. -// carts.sales_channel -``` +For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada. + -# Tax Lines in Cart Module +Service zones can be more restrictive, such as restricting to certain cities or province codes. -In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. + -## 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. +## Shipping Option Rules - +You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. -*** +These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rule: -## Tax Inclusivity +- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`. +- `operator`: The operator used in the condition. For example: + - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values. + - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values. + - Check out more operators in [this reference](https://docs.medusajs.com/references/fulfillment/types/fulfillment.RuleOperatorType/index.html.md). +- `value`: One or more values. -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. + -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. +A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g. -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 taxes perspective. +*** - +## Shipping Profile and Types -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`. +A shipping option belongs to a type. For example, a shipping option’s type may be `express`, while another `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md). + +A shipping option also belongs to a shipping profile, as each shipping profile defines the type of items to be shipped in a similar manner. *** -## Retrieve Tax Lines +## data Property -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. +When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process. -```ts -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.tax_lines", - "shipping_methods.tax_lines", - "shipping_address", - ], -}) +The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. -// 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", - }, - } -) -``` -Then, use the returned tax lines to set the line items and shipping methods’ tax lines: +# Inventory Concepts -```ts -// set line item tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "line_item_id" in line) -) +In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. -// set shipping method tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "shipping_line_id" in line) -) -``` +## 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. -# Promotions Adjustments in Carts +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. -In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. + -## What are Adjustment Lines? +### Inventory Shipping Requirement -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. +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. -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. +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). - +*** -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. +## 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. -## Discountable Option +It has three quantity-related properties: -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. +- `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. -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. +### 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. *** -## Promotion Actions +## ReservationItem -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. +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. -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). +The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. -For example: -```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - ComputeActionAdjustmentLine, - ComputeActionItemLine, - ComputeActionShippingLine, - // ... -} from "@medusajs/framework/types" +# Inventory Module in Medusa Flows -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.adjustments", - "shipping_methods.adjustments", - ], -}) +This document explains how the Inventory Module is used within the Medusa application's flows. -// 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, - }) - } -}) +## Product Variant Creation -// 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, - }) - } -}) +When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. -// compute actions -const actions = await promotionModuleService.computeActions( - ["promo_123"], - { - items: lineItemAdjustments, - shipping_methods: shippingMethodAdjustments, - } -) -``` +This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) -The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. + -Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments. +*** -```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - AddItemAdjustmentAction, - AddShippingMethodAdjustment, - // ... -} from "@medusajs/framework/types" +## Add to Cart -// ... +When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. -await cartModuleService.setLineItemAdjustments( - cart.id, - actions.filter( - (action) => action.action === "addItemAdjustment" - ) as AddItemAdjustmentAction[] -) +This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) -await cartModuleService.setShippingMethodAdjustments( - cart.id, - actions.filter( - (action) => - action.action === "addShippingMethodAdjustment" - ) as AddShippingMethodAdjustment[] -) -``` + +*** -# Customer Accounts +## Order Placed -In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. +When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. -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. +This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) -## `has_account` Property + -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. +*** -When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. +## Order Fulfillment -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`. +When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: + +- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. +- Resets the `reserved_quantity` to `0`. +- Deletes the associated reservation item. + +This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) + + *** -## Email Uniqueness +## Order Return -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. +When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. -So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. +This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) + -# Links between Customer Module and Other Modules +### Dismissed Returned Items -This document showcases the module links defined between the Customer Module and other commerce modules. +If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. + + +# Links between Inventory Module and Other Modules + +This document showcases the module links defined between the Inventory Module and other commerce modules. ## Summary -The Customer Module has the following links to other modules: +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. -- [`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). +- [`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). *** -## Payment Module +## Product 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. +Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. -This link is available starting from Medusa `v2.5.0`. + + +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 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`: +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 ```ts -const { data: customers } = await query.graph({ - entity: "customer", +const { data: inventoryItems } = await query.graph({ + entity: "inventory_item", fields: [ - "account_holder.*", + "variants.*", ], }) -// customers.account_holder +// inventoryItems.variants ``` ### useQueryGraphStep @@ -19322,19 +20394,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: customers } = useQueryGraphStep({ - entity: "customer", +const { data: inventoryItems } = useQueryGraphStep({ + entity: "inventory_item", fields: [ - "account_holder.*", + "variants.*", ], }) -// customers.account_holder +// inventoryItems.variants ``` ### Manage with Link -To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the variants of an inventory item, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -19344,11 +20416,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", }, }) ``` @@ -19356,81 +20428,42 @@ await link.create({ ### createRemoteLinkStep ```ts +import { Modules } from "@medusajs/framework/utils" import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", }, }) ``` *** -## Cart Module - -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. - -### Retrieve with Query - -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 - -```ts -const { data: customers } = await query.graph({ - entity: "customer", - fields: [ - "carts.*", - ], -}) - -// customers.carts -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: [ - "carts.*", - ], -}) - -// customers.carts -``` - -*** - -## Order Module +## Stock Location Module -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. +Medusa defines a read-only link between the `InventoryLevel` data model and the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md)'s `StockLocation` data model. This means you can retrieve the details of an inventory level's stock locations, 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 a customer's orders with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: +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: customers } = await query.graph({ - entity: "customer", +const { data: inventoryLevels } = await query.graph({ + entity: "inventory_level", fields: [ - "orders.*", + "stock_locations.*", ], }) -// customers.orders +// inventoryLevels.stock_locations ``` ### useQueryGraphStep @@ -19440,14 +20473,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: customers } = useQueryGraphStep({ - entity: "customer", +const { data: inventoryLevels } = useQueryGraphStep({ + entity: "inventory_level", fields: [ - "orders.*", + "stock_locations.*", ], }) -// customers.orders +// inventoryLevels.stock_locations ``` @@ -19839,476 +20872,345 @@ 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 Module in Medusa Flows +# Order Concepts -This document explains how the Inventory Module is used within the Medusa application's flows. +In this document, you’ll learn about orders and related concepts -## Product Variant Creation +## Order Items -When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. +The items purchased in the order are represented by the [OrderItem data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). An order can have multiple items. -This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) + - +### Item’s Product Details -*** +The details of the purchased products are represented by the [LineItem data model](https://docs.medusajs.com/references/order/models/OrderLineItem/index.html.md). Not only does a line item hold the details of the product, but also details related to its price, adjustments due to promotions, and taxes. -## Add to Cart +*** -When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. +## Order’s Shipping Method -This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) +An order has one or more shipping methods used to handle item shipment. - +Each shipping method is represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md) that holds its details. The shipping method is linked to the order through the [OrderShipping data model](https://docs.medusajs.com/references/order/models/OrderShipping/index.html.md). -*** + -## Order Placed +### data Property -When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. +When fulfilling the order, you can use a third-party fulfillment provider that requires additional custom data to be passed along from the order creation process. -This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) +The `OrderShippingMethod` data model has a `data` property. It’s an object used to store custom data relevant later for fulfillment. - +The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items. *** -## Order Fulfillment +## Order Totals -When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: +The order’s total amounts (including tax total, total after an item is returned, etc…) are represented by the [OrderSummary data model](https://docs.medusajs.com/references/order/models/OrderSummary/index.html.md). -- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. -- Resets the `reserved_quantity` to `0`. -- Deletes the associated reservation item. +*** -This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) +## Order Payments - +Payments made on an order, whether they’re capture or refund payments, are recorded as transactions represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). -*** +An order can have multiple transactions. The sum of these transactions must be equal to the order summary’s total. Otherwise, there’s an outstanding amount. -## Order Return +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). -When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. -This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) +# Order Claim - +In this document, you’ll learn about order claims. -### Dismissed Returned Items +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. -If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. +## What is a Claim? +When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. -# Inventory Concepts +The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. -In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. +*** -## InventoryItem +## Claim Type -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 `Claim` data model has a `type` property whose value indicates the type of the claim: -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. +- `refund`: the items are returned, and the customer is refunded. +- `replace`: the items are returned, and the customer receives new items. - +*** -### Inventory Shipping Requirement +## Old and Replacement Items -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 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. -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). +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). *** -## InventoryLevel +## Claim Shipping Methods -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. +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). -It has three quantity-related properties: +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). -- `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 +## Claim Refund -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. +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. *** -## ReservationItem +## How Claims Impact an Order’s Version -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. +When a claim is confirmed, the order’s version is incremented. -The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. +# Order Edit -# Links between Inventory Module and Other Modules +In this document, you'll learn about order edits. -This document showcases the module links defined between the Inventory Module and other commerce modules. +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. -## Summary +## What is an Order Edit? -The Inventory Module has the following links to other modules: +A merchant can edit an order to add new items or change the quantity of existing items in the order. -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md). -- [`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). +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`. *** -## Product Module +## Add Items in an Order Edit -Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. +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). - +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. -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. +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. -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 +## Update Items in an Order Edit -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`: +A merchant can update an existing item's quantity or price. -### query.graph - -```ts -const { data: inventoryItems } = await query.graph({ - entity: "inventory_item", - fields: [ - "variants.*", - ], -}) - -// inventoryItems.variants -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: inventoryItems } = useQueryGraphStep({ - entity: "inventory_item", - fields: [ - "variants.*", - ], -}) - -// inventoryItems.variants -``` - -### Manage with Link - -To manage the variants of an inventory item, 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.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", - }, -}) -``` +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. *** -## Stock Location Module - -Medusa defines a read-only link between the `InventoryLevel` data model and the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md)'s `StockLocation` data model. This means you can retrieve the details of an inventory level's stock locations, 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 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 -``` - - -# Links between Currency Module and Other Modules - -This document showcases the module links defined between the Currency Module and other commerce modules. - -## Summary - -The Currency Module has the following links to other modules: +## Shipping Methods of New Items in the Edit -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +Adding new items to the order requires adding shipping methods for those items. -- [`Currency` data model of Store Module \<> `Currency` data model of Currency Module](#store-module). (Read-only). +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` *** -## 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. +## How Order Edits Impact an Order’s Version -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. +When an order edit is confirmed, the order’s version is incremented. -### 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`: +## Payments and Refunds for Order Edit Changes -### query.graph +Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order. -```ts -const { data: stores } = await query.graph({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) +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). -// stores.supported_currencies -``` -### useQueryGraphStep +# Order Exchange -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +In this document, you’ll learn about order exchanges. -// ... +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard. -const { data: stores } = useQueryGraphStep({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) +## What is an Exchange? -// stores.supported_currencies -``` +An exchange is the replacement of an item that the customer ordered with another. +A merchant creates the exchange, specifying the items to be replaced and the new items to be sent. -# Fulfillment Module Provider +The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. -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. +## Returned and New Items -## What’s a Fulfillment Module Provider? +When the exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is created to handle receiving the items back from the customer. -A fulfillment module provider handles fulfilling items, typically using a third-party integration. +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). -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). +The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer. *** -## Configure Fulfillment Providers +## Exchange Shipping Methods -The Fulfillment Module accepts a `providers` option that allows you to register providers in your application. +An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). -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). +The shipping methods for the returned items are associated with the exchange'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 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 Concepts - -In this document, you’ll learn about some basic fulfillment concepts. - -## Fulfillment Set +## Exchange Payment -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. +The `Exchange` data model has a `difference_due` property that stores the outstanding amount. -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. +|Condition|Result| +|---|---|---| +|\`difference\_due \< 0\`|Merchant owes the customer a refund of the | +|\`difference\_due > 0\`|Merchant requires additional payment from the customer of the | +|\`difference\_due = 0\`|No payment processing is required.| -```ts -const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( - [ - { - name: "Shipping", - type: "shipping", - }, - { - name: "Pick-up", - type: "pick-up", - }, - ] -) -``` +Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). *** -## 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. - - - -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. +## How Exchanges Impact an Order’s Version -*** +When an exchange is confirmed, the order’s version is incremented. -## 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. +# Order Change -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. +In this document, you'll learn about the Order Change data model and possible actions in it. +## OrderChange Data Model -# Item Fulfillment +The [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md) represents any kind of change to an order, such as a return, exchange, or edit. -In this document, you’ll learn about the concepts of item fulfillment. +Its `change_type` property indicates what the order change is created for: -## Fulfillment Data Model +1. `edit`: The order change is making edits to the order, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md). +2. `exchange`: The order change is associated with an exchange, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md). +3. `claim`: The order change is associated with a claim, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md). +4. `return_request` or `return_receive`: The order change is associated with a return, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). -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). +Once the order change is confirmed, its changes are applied on the order. *** -## 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. +## Order Change Actions -The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. +The actions to perform on the original order by a change, such as adding an item, are represented by the [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md). - +The `OrderChangeAction` has an `action` property that indicates the type of action to perform on the order, and a `details` property that holds more details related to the action. -*** +The following table lists the possible `action` values that Medusa uses and what `details` they carry. -## data Property +|Action|Description|Details| +|---|---|---|---|---| +|\`ITEM\_ADD\`|Add an item to the order.|\`details\`| +|\`ITEM\_UPDATE\`|Update an item in the order.|\`details\`| +|\`RETURN\_ITEM\`|Set an item to be returned.|\`details\`| +|\`RECEIVE\_RETURN\_ITEM\`|Mark a return item as received.|\`details\`| +|\`RECEIVE\_DAMAGED\_RETURN\_ITEM\`|Mark a return item that's damaged as received.|\`details\`| +|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | +|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | +|\`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\`| -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. +# Links between Order Module and Other Modules -*** +This document showcases the module links defined between the Order Module and other commerce modules. -## Fulfillment Items +## Summary -A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model. +The Order Module has the following links to other modules: -The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill. +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). *** -## Fulfillment Label +## Customer Module -Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the `FulfillmentLabel` data model. +Medusa defines a read-only link between the `Order` 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 an order's customer, 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. -*** +### Retrieve with Query -## Fulfillment Status +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`: -The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment: +### query.graph -- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed. -- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped. -- `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered. +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "customer.*", + ], +}) +// orders.customer +``` -# Links between Fulfillment Module and Other Modules +### useQueryGraphStep -This document showcases the module links defined between the Fulfillment Module and other commerce modules. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -## Summary +// ... -The Fulfillment Module has the following links to other modules: +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "customer.*", + ], +}) -- [`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). +// orders.customer +``` *** -## 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 functionalities. - -Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items. +## Cart Module - +The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) provides cart-management features. -A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. +Medusa defines a link between the `Order` and `Cart` data models. The order is linked to the cart used for the purchased. - + ### Retrieve with Query -To retrieve the order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: - -To retrieve the return, pass `return.*` in `fields`. +To retrieve the cart of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`: ### query.graph ```ts -const { data: fulfillments } = await query.graph({ - entity: "fulfillment", +const { data: orders } = await query.graph({ + entity: "order", fields: [ - "order.*", + "cart.*", ], }) -// fulfillments.order +// orders.cart ``` ### useQueryGraphStep @@ -20318,19 +21220,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: fulfillments } = useQueryGraphStep({ - entity: "fulfillment", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ - "order.*", + "cart.*", ], }) -// fulfillments.order +// orders.cart ``` ### Manage with Link -To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the cart of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -20343,8 +21245,8 @@ await link.create({ [Modules.ORDER]: { order_id: "order_123", }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", + [Modules.CART]: { + cart_id: "cart_123", }, }) ``` @@ -20361,37 +21263,41 @@ createRemoteLinkStep({ [Modules.ORDER]: { order_id: "order_123", }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", + [Modules.CART]: { + cart_id: "cart_123", }, }) ``` *** -## Pricing Module +## Fulfillment Module -The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context. +A fulfillment is created for an orders' items. Medusa defines a link between the `Fulfillment` and `Order` data models. -Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. + - +A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. + + ### Retrieve with Query -To retrieve the price set of a shipping option with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: +To retrieve the fulfillments of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillments.*` in `fields`: + +To retrieve the fulfillments of a return, pass `fulfillments.*` in `fields`. ### query.graph ```ts -const { data: shippingOptions } = await query.graph({ - entity: "shipping_option", +const { data: orders } = await query.graph({ + entity: "order", fields: [ - "price_set.*", + "fulfillments.*", ], }) -// shippingOptions.price_set +// orders.fulfillments ``` ### useQueryGraphStep @@ -20401,19 +21307,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: shippingOptions } = useQueryGraphStep({ - entity: "shipping_option", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ - "price_set.*", + "fulfillments.*", ], }) -// shippingOptions.price_set +// orders.fulfillments ``` ### 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 fulfillments of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -20423,11 +21329,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", }, }) ``` @@ -20441,38 +21347,40 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", }, }) ``` *** -## Product Module +## Payment Module -Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile. +An order's payment details are stored in a payment collection. This also applies for claims and exchanges. -This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). +So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models. + + ### Retrieve with Query -To retrieve the products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: +To retrieve the payment collections of an order, order exchange, or order claim with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collections.*` in `fields`: ### query.graph ```ts -const { data: shippingProfiles } = await query.graph({ - entity: "shipping_profile", +const { data: orders } = await query.graph({ + entity: "order", fields: [ - "products.*", + "payment_collections.*", ], }) -// shippingProfiles.products +// orders.payment_collections ``` ### useQueryGraphStep @@ -20482,19 +21390,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: shippingProfiles } = useQueryGraphStep({ - entity: "shipping_profile", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ - "products.*", + "payment_collections.*", ], }) -// shippingProfiles.products +// orders.payment_collections ``` ### Manage with Link -To manage the shipping profile of a product, 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 @@ -20504,11 +21412,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", }, }) ``` @@ -20522,46 +21430,41 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", }, }) ``` *** -## Stock Location Module - -The Stock Location Module provides features to manage stock locations in a store. - -Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location. - - +## Product Module -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 read-only links between: - +- the `OrderLineItem` 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 `OrderLineItem` data model. +- the `OrderLineItem` 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 `OrderLineItem` data model. ### Retrieve with Query -To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` 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 stock location of a fulfillment provider, pass `locations.*` in `fields`. +To retrieve the product, pass `product.*` in `fields`. ### query.graph ```ts -const { data: fulfillmentSets } = await query.graph({ - entity: "fulfillment_set", +const { data: lineItems } = await query.graph({ + entity: "order_line_item", fields: [ - "location.*", + "variant.*", ], }) -// fulfillmentSets.location +// lineItems.variant ``` ### useQueryGraphStep @@ -20571,421 +21474,508 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: fulfillmentSets } = useQueryGraphStep({ - entity: "fulfillment_set", +const { data: lineItems } = useQueryGraphStep({ + entity: "order_line_item", fields: [ - "location.*", + "variant.*", ], }) -// fulfillmentSets.location +// lineItems.variant ``` -### 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): +## Promotion Module -### link.create +An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. -```ts -import { Modules } from "@medusajs/framework/utils" + -// ... +### Retrieve with Query -await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` +To retrieve the promotion applied on an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotion.*` in `fields`: -### createRemoteLinkStep +### query.graph ```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", - }, +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "promotion.*", + ], }) + +// orders.promotion ``` +### useQueryGraphStep -# Shipping Option +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -In this document, you’ll learn about shipping options and their rules. +// ... -## What’s a Shipping Option? +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "promotion.*", + ], +}) -A shipping option is a way of shipping an item. Each fulfillment provider provides a set of shipping options. For example, a provider may provide a shipping option for express shipping and another for standard shipping. +// orders.promotion +``` -When the customer places their order, they choose a shipping option to be used to fulfill their items. +### Manage with Link -A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). +To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -*** +### link.create -## Service Zone Restrictions +```ts +import { Modules } from "@medusajs/framework/utils" -A shipping option is restricted by a service zone, limiting the locations a shipping option be used in. +// ... -For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada. +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` - +### createRemoteLinkStep -Service zones can be more restrictive, such as restricting to certain cities or province codes. +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - +// ... -*** +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` -## Shipping Option Rules +*** -You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. +## Region Module -These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rule: +Medusa defines a read-only link between the `Order` 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 an order's region, 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. -- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`. -- `operator`: The operator used in the condition. For example: - - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values. - - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values. - - Check out more operators in [this reference](https://docs.medusajs.com/references/fulfillment/types/fulfillment.RuleOperatorType/index.html.md). -- `value`: One or more values. +### 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`: -A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g. +### query.graph - +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "region.*", + ], +}) -*** +// orders.region +``` -## Shipping Profile and Types +### useQueryGraphStep -A shipping option belongs to a type. For example, a shipping option’s type may be `express`, while another `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md). +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -A shipping option also belongs to a shipping profile, as each shipping profile defines the type of items to be shipped in a similar manner. +// ... -*** +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "region.*", + ], +}) -## data Property +// orders.region +``` -When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process. +*** -The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. +## Sales Channel Module +Medusa defines a read-only link between the `Order` 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 an order's sales channel, 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. -# Fulfillment Module Options +### Retrieve with Query -In this document, you'll learn about the options of the Fulfillment Module. +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`: -## providers +### query.graph -The `providers` option is an array of fulfillment module providers. +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "sales_channel.*", + ], +}) -When the Medusa application starts, these providers are registered and can be used to process fulfillments. +// orders.sales_channel +``` -For example: +### useQueryGraphStep -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/fulfillment", - options: { - providers: [ - { - resolve: `@medusajs/medusa/fulfillment-manual`, - id: "manual", - options: { - // provider options... - }, - }, - ], - }, - }, +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "sales_channel.*", ], }) -``` -The `providers` option is an array of objects that accept the following properties: +// orders.sales_channel +``` -- `resolve`: A string indicating either 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. +# Promotions Adjustments in Orders -# Order Concepts +In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines. -In this document, you’ll learn about orders and related concepts +## What are Adjustment Lines? -## Order Items +An adjustment line indicates a change to a line item or a shipping method’s amount. It’s used to apply promotions or discounts on an order. -The items purchased in the order are represented by the [OrderItem data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). An order can have multiple items. +The [OrderLineItemAdjustment data model](https://docs.medusajs.com/references/order/models/OrderLineItemAdjustment/index.html.md) represents changes on a line item, and the [OrderShippingMethodAdjustment data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodAdjustment/index.html.md) represents changes on a shipping method. - + -### Item’s Product Details +The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. -The details of the purchased products are represented by the [LineItem data model](https://docs.medusajs.com/references/order/models/OrderLineItem/index.html.md). Not only does a line item hold the details of the product, but also details related to its price, adjustments due to promotions, and taxes. +The ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. *** -## Order’s Shipping Method +## Discountable Option -An order has one or more shipping methods used to handle item shipment. +The `OrderLineItem` data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. -Each shipping method is represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md) that holds its details. The shipping method is linked to the order through the [OrderShipping data model](https://docs.medusajs.com/references/order/models/OrderShipping/index.html.md). +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. - +*** -### data Property +## Promotion Actions -When fulfilling the order, you can use a third-party fulfillment provider that requires additional custom data to be passed along from the order creation process. +When using the Order and Promotion modules together, 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. -The `OrderShippingMethod` data model has a `data` property. It’s an object used to store custom data relevant later for fulfillment. +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). -The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items. +```ts collapsibleLines="1-10" expandButtonLabel="Show Imports" +import { + ComputeActionAdjustmentLine, + ComputeActionItemLine, + ComputeActionShippingLine, + // ... +} from "@medusajs/framework/types" -*** +// ... -## Order Totals +// retrieve the order +const order = await orderModuleService.retrieveOrder("ord_123", { + relations: [ + "items.item.adjustments", + "shipping_methods.shipping_method.adjustments", + ], +}) +// retrieve the line item adjustments +const lineItemAdjustments: ComputeActionItemLine[] = [] +order.items.forEach((item) => { + const filteredAdjustments = item.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + lineItemAdjustments.push({ + ...item, + ...item.detail, + adjustments: filteredAdjustments, + }) + } +}) -The order’s total amounts (including tax total, total after an item is returned, etc…) are represented by the [OrderSummary data model](https://docs.medusajs.com/references/order/models/OrderSummary/index.html.md). +//retrieve shipping method adjustments +const shippingMethodAdjustments: ComputeActionShippingLine[] = + [] +order.shipping_methods.forEach((shippingMethod) => { + const filteredAdjustments = + shippingMethod.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + shippingMethodAdjustments.push({ + ...shippingMethod, + adjustments: filteredAdjustments, + }) + } +}) -*** +// compute actions +const actions = await promotionModuleService.computeActions( + ["promo_123"], + { + items: lineItemAdjustments, + shipping_methods: shippingMethodAdjustments, + // TODO infer from cart or region + currency_code: "usd", + } +) +``` -## Order Payments +The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. -Payments made on an order, whether they’re capture or refund payments, are recorded as transactions represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). +Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the order’s line items and the shipping method’s adjustments. -An order can have multiple transactions. The sum of these transactions must be equal to the order summary’s total. Otherwise, there’s an outstanding amount. +```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + AddItemAdjustmentAction, + AddShippingMethodAdjustment, + // ... +} from "@medusajs/framework/types" -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). +// ... +await orderModuleService.setOrderLineItemAdjustments( + order.id, + actions.filter( + (action) => action.action === "addItemAdjustment" + ) as AddItemAdjustmentAction[] +) -# Order Claim +await orderModuleService.setOrderShippingMethodAdjustments( + order.id, + actions.filter( + (action) => + action.action === "addShippingMethodAdjustment" + ) as AddShippingMethodAdjustment[] +) +``` -In this document, you’ll learn about order claims. -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. +# Order Versioning -## What is a Claim? +In this document, you’ll learn how an order and its details are versioned. -When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. +## What's Versioning? -The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. +Versioning means assigning a version number to a record, such as an order and its items. This is useful to view the different versions of the order following changes in its lifetime. + +When changes are made on an order, such as an item is added or returned, the order's version changes. *** -## Claim Type +## version Property -The `Claim` data model has a `type` property whose value indicates the type of the claim: +The `Order` and `OrderSummary` data models have a `version` property that indicates the current version. By default, its value is `1`. -- `refund`: the items are returned, and the customer is refunded. -- `replace`: the items are returned, and the customer receives new items. +Other order-related data models, such as `OrderItem`, also has a `version` property, but it indicates the version it belongs to. *** -## 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. - -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). +## How the Version Changes -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). +When the order is changed, such as an item is exchanged, this changes the version of the order and its related data: -*** +1. The version of the order and its summary is incremented. +2. Related order data that have a `version` property, such as the `OrderItem`, are duplicated. The duplicated item has the new version, whereas the original item has the previous version. -## Claim Shipping Methods +When the order is retrieved, only the related data having the same version is retrieved. -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). -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). +# Order Return -*** +In this document, you’ll learn about order returns. -## Claim Refund +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard. -If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property. +## What is a Return? -The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim. +A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md). -*** +A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow. -## How Claims Impact an Order’s Version + -When a claim is confirmed, the order’s version is incremented. +Once the merchant receives the returned items, they mark the return as received. +*** -# Order Edit +## Returned Items -In this document, you'll learn about order edits. +The items to be returned are represented by the [ReturnItem data model](references/order/models/ReturnItem). -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. +The `ReturnItem` model has two properties storing the item's quantity: -## What is an Order Edit? +1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity. +2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity. -A merchant can edit an order to add new items or change the quantity of existing items in the order. +*** -An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md). +## Return Shipping Methods -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. +A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](references/order/models/OrderShippingMethod). -In the case of an order edit, the `OrderChange`'s type is `edit`. +In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled. *** -## 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). +## Refund Payment -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 `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer. -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. +The [OrderTransaction data model](references/order/models/OrderTransaction) represents the refunds made for the return. *** -## Update Items in an Order Edit +## Returns in Exchanges and Claims -A merchant can update an existing item's quantity or price. +When a merchant creates an exchange or a claim, it includes returning 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. +The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for. *** -## Shipping Methods of New Items in the Edit +## How Returns Impact an Order’s Version -Adding new items to the order requires adding shipping methods for those items. +The order’s version is incremented when: -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` +1. A return is requested. +2. A return is marked as received. -*** -## How Order Edits Impact an Order’s Version +# Tax Lines in Order Module -When an order edit is confirmed, the order’s version is incremented. +In this document, you’ll learn about tax lines in an order. -*** +## What are Tax Lines? -## Payments and Refunds for Order Edit Changes +A tax line indicates the tax rate of a line item or a shipping method. -Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order. +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. -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 +## Tax Inclusivity -In this document, you’ll learn about order exchanges. +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. -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard. +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. -## What is an Exchange? +So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. -An exchange is the replacement of an item that the customer ordered with another. +The following diagram is a simplified showcase of how a subtotal is calculated from the tax perspective. -A merchant creates the exchange, specifying the items to be replaced and the new items to be sent. + -The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. +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`. -*** -## Returned and New Items +# Transactions -When the exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is created to handle receiving the items back from the customer. +In this document, you’ll learn about an order’s transactions and its use. -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). +## What is a Transaction? -The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer. +A transaction represents any order payment process, such as capturing or refunding an amount. It’s represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). -*** +The transaction’s main purpose is to ensure a correct balance between paid and outstanding amounts. -## Exchange Shipping Methods +Transactions are also associated with returns, claims, and exchanges if additional payment or refund is required. -An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). +*** -The shipping methods for the returned items are associated with the exchange'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). +## Checking Outstanding Amount -*** +The order’s total amounts are stored in the `OrderSummary`'s `totals` property, which is a JSON object holding the total details of the order. -## Exchange Payment +```json +{ + "totals": { + "total": 30, + "subtotal": 30, + // ... + } +} +``` -The `Exchange` data model has a `difference_due` property that stores the outstanding amount. +To check the outstanding amount of the order, its transaction amounts are summed. Then, the following conditions are checked: |Condition|Result| |---|---|---| -|\`difference\_due \< 0\`|Merchant owes the customer a refund of the | -|\`difference\_due > 0\`|Merchant requires additional payment from the customer of the | -|\`difference\_due = 0\`|No payment processing is required.| - -Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). +|summary’s total - transaction amounts total = 0|There’s no outstanding amount.| +|summary’s total - transaction amounts total > 0|The customer owes additional payment to the merchant.| +|summary’s total - transaction amounts total \< 0|The merchant owes the customer a refund.| *** -## How Exchanges Impact an Order’s Version +## Transaction Reference -When an exchange is confirmed, the order’s version is incremented. +The Order Module doesn’t provide payment processing functionalities, so it doesn’t store payments that can be processed. Payment functionalities are provided by the [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md). +The `OrderTransaction` data model has two properties that determine which data model and record holds the actual payment’s details: -# Links between Order Module and Other Modules +- `reference`: indicates the table’s name in the database. For example, `payment` from the Payment Module. +- `reference_id`: indicates the ID of the record in the table. For example, `pay_123`. -This document showcases the module links defined between the Order Module and other commerce modules. -## Summary +# Links between Payment Module and Other Modules -The Order Module has the following links to other modules: +This document showcases the module links defined between the Payment Module and other commerce modules. -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +## Summary -- [`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). +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). *** -## Customer Module +## Cart Module -Medusa defines a read-only link between the `Order` 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 an order's customer, 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. +The Cart Module provides cart-related features, but not payment processing. + +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. + +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 -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`: +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 ```ts -const { data: orders } = await query.graph({ - entity: "order", +const { data: paymentCollections } = await query.graph({ + entity: "payment_collection", fields: [ - "customer.*", + "cart.*", ], }) -// orders.customer +// paymentCollections.cart ``` ### useQueryGraphStep @@ -20995,63 +21985,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: paymentCollections } = useQueryGraphStep({ + entity: "payment_collection", fields: [ - "customer.*", + "cart.*", ], }) -// orders.customer +// paymentCollections.cart ``` -*** +### Manage with Link -## Cart Module - -The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) provides cart-management features. - -Medusa defines a link between the `Order` and `Cart` data models. The order is linked to the cart used for the purchased. - - - -### Retrieve with Query - -To retrieve the cart of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`: - -### query.graph - -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "cart.*", - ], -}) - -// orders.cart -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "cart.*", - ], -}) - -// orders.cart -``` - -### Manage with Link - -To manage the cart of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -21061,62 +22007,55 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, [Modules.CART]: { cart_id: "cart_123", }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, }) ``` ### createRemoteLinkStep ```ts -import { Modules } from "@medusajs/framework/utils" import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, [Modules.CART]: { cart_id: "cart_123", }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, }) ``` *** -## Fulfillment Module - -A fulfillment is created for an orders' items. Medusa defines a link between the `Fulfillment` and `Order` data models. - - +## Customer Module -A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. +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`. ### Retrieve with Query -To retrieve the fulfillments of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillments.*` in `fields`: - -To retrieve the fulfillments of a return, pass `fulfillments.*` 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: orders } = await query.graph({ - entity: "order", +const { data: accountHolders } = await query.graph({ + entity: "account_holder", fields: [ - "fulfillments.*", + "customer.*", ], }) -// orders.fulfillments +// accountHolders.customer ``` ### useQueryGraphStep @@ -21126,19 +22065,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: accountHolders } = useQueryGraphStep({ + entity: "account_holder", fields: [ - "fulfillments.*", + "customer.*", ], }) -// orders.fulfillments +// accountHolders.customer ``` ### Manage with Link -To manage the fulfillments of an order, 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 @@ -21148,11 +22087,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.CUSTOMER]: { + customer_id: "cus_123", }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", }, }) ``` @@ -21160,24 +22099,23 @@ await link.create({ ### createRemoteLinkStep ```ts -import { Modules } from "@medusajs/framework/utils" import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.CUSTOMER]: { + customer_id: "cus_123", }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", }, }) ``` *** -## Payment Module +## Order Module An order's payment details are stored in a payment collection. This also applies for claims and exchanges. @@ -21187,19 +22125,19 @@ So, Medusa defines links between the `PaymentCollection` data model and the `Ord ### Retrieve with Query -To retrieve the payment collections of an order, order exchange, or order claim with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collections.*` 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: orders } = await query.graph({ - entity: "order", +const { data: paymentCollections } = await query.graph({ + entity: "payment_collection", fields: [ - "payment_collections.*", + "order.*", ], }) -// orders.payment_collections +// paymentCollections.order ``` ### useQueryGraphStep @@ -21209,14 +22147,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: paymentCollections } = useQueryGraphStep({ + entity: "payment_collection", fields: [ - "payment_collections.*", + "order.*", ], }) -// orders.payment_collections +// paymentCollections.order ``` ### Manage with Link @@ -21260,72 +22198,29 @@ createRemoteLinkStep({ *** -## Product Module - -Medusa defines read-only links between: - -- the `OrderLineItem` 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 `OrderLineItem` data model. -- the `OrderLineItem` 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 `OrderLineItem` 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`: - -To retrieve the product, pass `product.*` in `fields`. - -### query.graph - -```ts -const { data: lineItems } = await query.graph({ - entity: "order_line_item", - fields: [ - "variant.*", - ], -}) - -// lineItems.variant -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: lineItems } = useQueryGraphStep({ - entity: "order_line_item", - fields: [ - "variant.*", - ], -}) - -// lineItems.variant -``` - -*** +## Region Module -## Promotion Module +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. -An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. + - +This increases the flexibility of your store. For example, you only show during checkout the payment providers associated with the cart's region. ### Retrieve with Query -To retrieve the promotion applied on an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotion.*` in `fields`: +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: orders } = await query.graph({ - entity: "order", +const { data: paymentProviders } = await query.graph({ + entity: "payment_provider", fields: [ - "promotion.*", + "regions.*", ], }) -// orders.promotion +// paymentProviders.regions ``` ### useQueryGraphStep @@ -21335,19 +22230,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: paymentProviders } = useQueryGraphStep({ + entity: "payment_provider", fields: [ - "promotion.*", + "regions.*", ], }) -// orders.promotion +// paymentProviders.regions ``` ### Manage with Link -To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +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 @@ -21357,11 +22252,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.REGION]: { + region_id: "reg_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", }, }) ``` @@ -21375,423 +22270,469 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.REGION]: { + region_id: "reg_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", }, }) ``` -*** -## Region Module +# Account Holders and Saved Payment Methods -Medusa defines a read-only link between the `Order` 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 an order's region, 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. +In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. -### Retrieve with Query +Account holders are available starting from Medusa `v2.5.0`. -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`: +## What's an Account Holder? -### query.graph +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. -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "region.*", - ], -}) +It holds fields retrieved from the third-party provider, such as: -// orders.region -``` +- `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. -### useQueryGraphStep +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 -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +*** -// ... +## Save Payment Methods -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "region.*", - ], -}) +If a payment provider supports saving payment methods for a customer, they must implement the following methods: -// orders.region -``` +- `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. + +Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). *** -## Sales Channel Module +## Account Holder in Medusa Payment Flows -Medusa defines a read-only link between the `Order` 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 an order's sales channel, 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. +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. -### Retrieve with Query +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. -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`: +This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). -### query.graph -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "sales_channel.*", - ], -}) +# Payment Module Options -// orders.sales_channel -``` +In this document, you'll learn about the options of the Payment Module. -### useQueryGraphStep +## All Module Options -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +|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|-| + +*** + +## 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. + +For example: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "sales_channel.*", +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/payment", + options: { + providers: [ + { + resolve: "@medusajs/medusa/payment-stripe", + id: "stripe", + options: { + // ... + }, + }, + ], + }, + }, ], }) - -// orders.sales_channel ``` +The `providers` option is an array of objects that accept the following properties: -# Order Versioning - -In this document, you’ll learn how an order and its details are versioned. +- `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. -## What's Versioning? -Versioning means assigning a version number to a record, such as an order and its items. This is useful to view the different versions of the order following changes in its lifetime. +# Payment -When changes are made on an order, such as an item is added or returned, the order's version changes. +In this document, you’ll learn what a payment is and how it's created, captured, and refunded. -*** +## What's a Payment? -## version Property +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. -The `Order` and `OrderSummary` data models have a `version` property that indicates the current version. By default, its value is `1`. +A payment carries many of the data and relations of a payment session: -Other order-related data models, such as `OrderItem`, also has a `version` property, but it indicates the version it belongs to. +- 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. *** -## How the Version Changes - -When the order is changed, such as an item is exchanged, this changes the version of the order and its related data: +## Capture Payments -1. The version of the order and its summary is incremented. -2. Related order data that have a `version` property, such as the `OrderItem`, are duplicated. The duplicated item has the new version, whereas the original item has the previous version. +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. -When the order is retrieved, only the related data having the same version is retrieved. +The payment can also be captured incrementally, each time a capture record is created for that amount. + -# Promotions Adjustments in Orders +*** -In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines. +## Refund Payments -## What are Adjustment Lines? +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. -An adjustment line indicates a change to a line item or a shipping method’s amount. It’s used to apply promotions or discounts on an order. +A payment can be refunded multiple times, and each time a refund record is created. -The [OrderLineItemAdjustment data model](https://docs.medusajs.com/references/order/models/OrderLineItemAdjustment/index.html.md) represents changes on a line item, and the [OrderShippingMethodAdjustment data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodAdjustment/index.html.md) represents changes on a shipping method. + - -The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. +# Accept Payment Flow -The ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. +In this document, you’ll learn how to implement an accept-payment flow using workflows or the Payment Module's main service. -*** +It's highly recommended to use Medusa's workflows to implement this flow. Use the Payment Module's main service for more complex cases. -## Discountable Option +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). -The `OrderLineItem` data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. +## Flow Overview -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. + *** -## Promotion Actions - -When using the Order and Promotion modules together, 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. +## 1. Create a Payment Collection -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). +A payment collection holds all details related to a resource’s payment operations. So, you start off by creating a payment collection. -```ts collapsibleLines="1-10" expandButtonLabel="Show Imports" -import { - ComputeActionAdjustmentLine, - ComputeActionItemLine, - ComputeActionShippingLine, - // ... -} from "@medusajs/framework/types" +For example: -// ... +### Using Workflow -// retrieve the order -const order = await orderModuleService.retrieveOrder("ord_123", { - relations: [ - "items.item.adjustments", - "shipping_methods.shipping_method.adjustments", - ], -}) -// retrieve the line item adjustments -const lineItemAdjustments: ComputeActionItemLine[] = [] -order.items.forEach((item) => { - const filteredAdjustments = item.adjustments?.filter( - (adjustment) => adjustment.code !== undefined - ) as unknown as ComputeActionAdjustmentLine[] - if (filteredAdjustments.length) { - lineItemAdjustments.push({ - ...item, - ...item.detail, - adjustments: filteredAdjustments, - }) - } -}) +```ts +import { createPaymentCollectionForCartWorkflow } from "@medusajs/medusa/core-flows" -//retrieve shipping method adjustments -const shippingMethodAdjustments: ComputeActionShippingLine[] = - [] -order.shipping_methods.forEach((shippingMethod) => { - const filteredAdjustments = - shippingMethod.adjustments?.filter( - (adjustment) => adjustment.code !== undefined - ) as unknown as ComputeActionAdjustmentLine[] - if (filteredAdjustments.length) { - shippingMethodAdjustments.push({ - ...shippingMethod, - adjustments: filteredAdjustments, - }) - } -}) +// ... -// compute actions -const actions = await promotionModuleService.computeActions( - ["promo_123"], - { - items: lineItemAdjustments, - shipping_methods: shippingMethodAdjustments, - // TODO infer from cart or region - currency_code: "usd", - } -) +await createPaymentCollectionForCartWorkflow(req.scope) + .run({ + input: { + cart_id: "cart_123", + }, + }) ``` -The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. - -Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the order’s line items and the shipping method’s adjustments. - -```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - AddItemAdjustmentAction, - AddShippingMethodAdjustment, - // ... -} from "@medusajs/framework/types" - -// ... - -await orderModuleService.setOrderLineItemAdjustments( - order.id, - actions.filter( - (action) => action.action === "addItemAdjustment" - ) as AddItemAdjustmentAction[] -) +### Using Service -await orderModuleService.setOrderShippingMethodAdjustments( - order.id, - actions.filter( - (action) => - action.action === "addShippingMethodAdjustment" - ) as AddShippingMethodAdjustment[] -) +```ts +const paymentCollection = + await paymentModuleService.createPaymentCollections({ + currency_code: "usd", + amount: 5000, + }) ``` +*** -# Order Change +## 2. Create Payment Sessions -In this document, you'll learn about the Order Change data model and possible actions in it. +The payment collection has one or more payment sessions, each being a payment amount to be authorized by a payment provider. -## OrderChange Data Model +So, after creating the payment collection, create at least one payment session for a provider. -The [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md) represents any kind of change to an order, such as a return, exchange, or edit. +For example: -Its `change_type` property indicates what the order change is created for: +### Using Workflow -1. `edit`: The order change is making edits to the order, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md). -2. `exchange`: The order change is associated with an exchange, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md). -3. `claim`: The order change is associated with a claim, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md). -4. `return_request` or `return_receive`: The order change is associated with a return, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). +```ts +import { createPaymentSessionsWorkflow } from "@medusajs/medusa/core-flows" -Once the order change is confirmed, its changes are applied on the order. +// ... -*** +const { result: paymentSesion } = await createPaymentSessionsWorkflow(req.scope) + .run({ + input: { + payment_collection_id: "paycol_123", + provider_id: "stripe", + }, + }) +``` -## Order Change Actions +### Using Service -The actions to perform on the original order by a change, such as adding an item, are represented by the [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md). +```ts +const paymentSession = + await paymentModuleService.createPaymentSession( + paymentCollection.id, + { + provider_id: "stripe", + currency_code: "usd", + amount: 5000, + data: { + // any necessary data for the + // payment provider + }, + } + ) +``` -The `OrderChangeAction` has an `action` property that indicates the type of action to perform on the order, and a `details` property that holds more details related to the action. +*** -The following table lists the possible `action` values that Medusa uses and what `details` they carry. +## 3. Authorize Payment Session -|Action|Description|Details| -|---|---|---|---|---| -|\`ITEM\_ADD\`|Add an item to the order.|\`details\`| -|\`ITEM\_UPDATE\`|Update an item in the order.|\`details\`| -|\`RETURN\_ITEM\`|Set an item to be returned.|\`details\`| -|\`RECEIVE\_RETURN\_ITEM\`|Mark a return item as received.|\`details\`| -|\`RECEIVE\_DAMAGED\_RETURN\_ITEM\`|Mark a return item that's damaged as received.|\`details\`| -|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | -|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | -|\`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\`| +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. +For example: -# Order Return +### Using Step -In this document, you’ll learn about order returns. +```ts +import { authorizePaymentSessionStep } from "@medusajs/medusa/core-flows" -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard. +// ... -## What is a Return? +authorizePaymentSessionStep({ + id: "payses_123", + context: {}, +}) +``` -A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md). +### Using Service -A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow. +```ts +const payment = authorizePaymentSessionStep({ + id: "payses_123", + context: {}, +}) +``` - +When the payment authorization is successful, a payment is created and returned. -Once the merchant receives the returned items, they mark the return as received. +### Handling Additional Action + +If you used the `authorizePaymentSessionStep`, you don't need to implement this logic as it's implemented in the step. + +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. + +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. + +For example: + +```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] + + if (updatedPaymentSession.status === "requires_more") { + // TODO perform required action + // TODO authorize payment again. + } +} +``` *** -## Returned Items +## 4. Payment Flow Complete -The items to be returned are represented by the [ReturnItem data model](references/order/models/ReturnItem). +The payment flow is complete once the payment session is authorized and the payment is created. -The `ReturnItem` model has two properties storing the item's quantity: +You can then: -1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity. -2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity. +- 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). + +Some payment providers allow capturing the payment automatically once it’s authorized. In that case, you don’t need to do it manually. + + +# Payment Module Provider + +In this document, you’ll learn what a payment module provider is. + +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. + +## What's a Payment Module Provider? + +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. + +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. + +### List of Payment Module Providers + +- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md) *** -## Return Shipping Methods +## System Payment Provider -A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](references/order/models/OrderShippingMethod). +The Payment Module provides a `system` payment provider that acts as a placeholder payment provider. -In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled. +It doesn’t handle payment processing and delegates that to the merchant. It acts similarly to a cash-on-delivery (COD) payment method. *** -## Refund Payment +## How are Payment Providers Created? -The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer. +A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`. -The [OrderTransaction data model](references/order/models/OrderTransaction) represents the refunds made for the return. +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. *** -## Returns in Exchanges and Claims +## Configure Payment Providers -When a merchant creates an exchange or a claim, it includes returning items from the customer. +The Payment Module accepts a `providers` option that allows you to register providers in your application. -The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for. +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). *** -## How Returns Impact an Order’s Version +## PaymentProvider Data Model -The order’s version is incremented when: +When the Medusa application starts and registers the payment providers, it also creates a record of the `PaymentProvider` data model if none exists. -1. A return is requested. -2. A return is marked as received. +This data model is used to reference a payment provider and determine whether it’s installed in the application. -# Transactions +# Payment Collection -In this document, you’ll learn about an order’s transactions and its use. +In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module. -## What is a Transaction? +## What's a Payment Collection? -A transaction represents any order payment process, such as capturing or refunding an amount. It’s represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). +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). -The transaction’s main purpose is to ensure a correct balance between paid and outstanding amounts. +Every purchase or request for payment starts with a payment collection. The collection holds details necessary to complete the payment, including: -Transactions are also associated with returns, claims, and exchanges if additional payment or refund is required. +- 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. *** -## Checking Outstanding Amount +## Multiple Payments -The order’s total amounts are stored in the `OrderSummary`'s `totals` property, which is a JSON object holding the total details of the order. +The payment collection supports multiple payment sessions and payments. -```json -{ - "totals": { - "total": 30, - "subtotal": 30, - // ... - } -} -``` +You can use this to accept payments in increments or split payments across payment providers. -To check the outstanding amount of the order, its transaction amounts are summed. Then, the following conditions are checked: + -|Condition|Result| -|---|---|---| -|summary’s total - transaction amounts total = 0|There’s no outstanding amount.| -|summary’s total - transaction amounts total > 0|The customer owes additional payment to the merchant.| -|summary’s total - transaction amounts total \< 0|The merchant owes the customer a refund.| +*** + +## Usage with the Cart Module + +The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment. + +During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing. + +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). + + + + +# 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. *** -## Transaction Reference +## getWebhookActionAndData Method -The Order Module doesn’t provide payment processing functionalities, so it doesn’t store payments that can be processed. Payment functionalities are provided by the [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md). +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. -The `OrderTransaction` data model has two properties that determine which data model and record holds the actual payment’s details: +Medusa implements a webhook listener route at the `/hooks/payment/[identifier]_[provider]` API route, where: -- `reference`: indicates the table’s name in the database. For example, `payment` from the Payment Module. -- `reference_id`: indicates the ID of the record in the table. For example, `pay_123`. +- `[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`. -# Tax Lines in Order Module +Use that webhook listener in your third-party payment provider's configurations. -In this document, you’ll learn about tax lines in an order. + -## What are Tax Lines? +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. -A tax line indicates the tax rate of a line item or a shipping method. +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. -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. +### Actions After Webhook Payment Processing - +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. + + +# Payment Session + +In this document, you’ll learn what a payment session is. + +## What's a Payment Session? + +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. + +A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. + + *** -## Tax Inclusivity +## data Property -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. +Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data. -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. +For example, the customer's ID in Stripe is stored in the `data` property. -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. +## Payment Session Status - +The `status` property of a payment session indicates its current status. Its value can be: -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`. +- `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. # Pricing Concepts @@ -21999,6 +22940,37 @@ createRemoteLinkStep({ ``` +# Price Rules + +In this document, you'll learn about price rules for price sets and price lists. + +## Price Rule + +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. + +For exmaple, you create a price restricted to `10557` zip codes. + + + +A price can have multiple price rules. + +For example, a price can be restricted by a region and a zip code. + + + +*** + +## Price List Rules + +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. + + + + # Prices Calculation 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. @@ -22192,37 +23164,6 @@ const price = await pricingModuleService.calculatePrices( ### Result -# Price Rules - -In this document, you'll learn about price rules for price sets and price lists. - -## Price Rule - -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. - -For exmaple, you create a price restricted to `10557` zip codes. - - - -A price can have multiple price rules. - -For example, a price can be restricted by a region and a zip code. - - - -*** - -## Price List Rules - -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. - - - - # Tax-Inclusive Pricing In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. @@ -22735,6 +23676,57 @@ createRemoteLinkStep({ ``` +# Configure Selling Products + +In this guide, you'll learn how to set up and configure your products based on their shipping and inventory requirements, the product type, how you want to sell them, or your commerce ecosystem. + +The concepts in this guide are applicable starting from Medusa v2.5.1. + +## Scenario + +Businesses can have different selling requirements: + +1. They may sell physical or digital items. +2. They may sell items that don't require shipping or inventory management, such as selling digital products, services, or booking appointments. +3. They may sell items whose inventory is managed by an external system, such as an ERP. + +Medusa supports these different selling requirements by allowing you to configure shipping and inventory requirements for products and their variants. This guide explains how these configurations work, then provides examples of setting up different use cases. + +*** + +## Configuring Shipping Requirements + +The Medusa application defines a link between the `Product` data model and a [ShippingProfile](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts#shipping-profile/index.html.md) in the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md), allowing you to associate a product with a shipping profile. + +When a product is associated with a shipping profile, its variants require shipping and fulfillment when purchased. This is useful for physical products or digital products that require custom fulfillment. + +If a product doesn't have an associated shipping profile, its variants don't require shipping and fulfillment when purchased. This is useful for digital products, for example, that don't require shipping. + +### Overriding Shipping Requirements for Variants + +A product variant whose inventory is managed by Medusa (its `manage_inventory` property is enabled) has an [inventory item](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventoryitem/index.html.md). The inventory item has a `requires_shipping` property that can be used to override its shipping requirement. This is useful if the product has an associated shipping profile but you want to disable shipping for a specific variant, or vice versa. + +Learn more about product variant's inventory in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). + +When a product variant is purchased, the Medusa application decides whether the purchased item requires shipping in the following order: + +1. The product variant has an inventory item. In this case, the Medusa application uses the inventory item's `requires_shipping` property to determine if the item requires shipping. +2. If the product variant doesn't have an inventory item, the Medusa application checks whether the product has an associated shipping profile to determine if the item requires shipping. + +*** + +## Use Case Examples + +By combining configurations of shipment requirements and inventory management, you can set up your products to support your use case: + +|Use Case|Configurations|Example| +|---|---|---|---|---| +|Item that's shipped on purchase, and its variant inventory is managed by the Medusa application.||Any stock-kept item (clothing, for example), whose inventory is managed in the Medusa application.| +|Item that's shipped on purchase, but its variant inventory is managed externally (not by Medusa) or it has infinite stock.||Any stock-kept item (clothing, for example), whose inventory is managed in an ERP or has infinite stock.| +|Item that's not shipped on purchase, but its variant inventory is managed by Medusa.||Digital products, such as licenses, that don't require shipping but have a limited quantity.| +|Item that doesn't require shipping and its variant inventory isn't managed by Medusa.||| + + # Product Variant Inventory # Product Variant Inventory @@ -22801,173 +23793,225 @@ The following guides provide more details on inventory management in the Medusa - [Storefront guide: how to retrieve a product variant's inventory details](https://docs.medusajs.com/resources/storefront-development/products/inventory/index.html.md). -# Account Holders and Saved Payment Methods +# Links between Region Module and Other Modules -In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. +This document showcases the module links defined between the Region Module and other commerce modules. -Account holders are available starting from Medusa `v2.5.0`. +## Summary -## What's an Account Holder? +The Region Module has the following links to other modules: -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. +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -It holds fields retrieved from the third-party provider, such as: +- [`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). -- `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. +*** -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. +## Cart Module -*** +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. -## Save Payment Methods +### Retrieve with Query -If a payment provider supports saving payment methods for a customer, they must implement the following methods: +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`: -- `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. +### query.graph -Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). +```ts +const { data: regions } = await query.graph({ + entity: "region", + fields: [ + "carts.*", + ], +}) -*** +// regions.carts +``` -## Account Holder in Medusa Payment Flows +### useQueryGraphStep -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. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -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. +// ... -This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). +const { data: regions } = useQueryGraphStep({ + entity: "region", + fields: [ + "carts.*", + ], +}) +// regions.carts +``` -# Configure Selling Products +*** -In this guide, you'll learn how to set up and configure your products based on their shipping and inventory requirements, the product type, how you want to sell them, or your commerce ecosystem. +## Order Module -The concepts in this guide are applicable starting from Medusa v2.5.1. +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. -## Scenario +### Retrieve with Query -Businesses can have different selling requirements: +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`: -1. They may sell physical or digital items. -2. They may sell items that don't require shipping or inventory management, such as selling digital products, services, or booking appointments. -3. They may sell items whose inventory is managed by an external system, such as an ERP. +### query.graph -Medusa supports these different selling requirements by allowing you to configure shipping and inventory requirements for products and their variants. This guide explains how these configurations work, then provides examples of setting up different use cases. +```ts +const { data: regions } = await query.graph({ + entity: "region", + fields: [ + "orders.*", + ], +}) -*** +// regions.orders +``` -## Configuring Shipping Requirements +### useQueryGraphStep -The Medusa application defines a link between the `Product` data model and a [ShippingProfile](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts#shipping-profile/index.html.md) in the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md), allowing you to associate a product with a shipping profile. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -When a product is associated with a shipping profile, its variants require shipping and fulfillment when purchased. This is useful for physical products or digital products that require custom fulfillment. +// ... -If a product doesn't have an associated shipping profile, its variants don't require shipping and fulfillment when purchased. This is useful for digital products, for example, that don't require shipping. +const { data: regions } = useQueryGraphStep({ + entity: "region", + fields: [ + "orders.*", + ], +}) -### Overriding Shipping Requirements for Variants +// regions.orders +``` -A product variant whose inventory is managed by Medusa (its `manage_inventory` property is enabled) has an [inventory item](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventoryitem/index.html.md). The inventory item has a `requires_shipping` property that can be used to override its shipping requirement. This is useful if the product has an associated shipping profile but you want to disable shipping for a specific variant, or vice versa. +*** -Learn more about product variant's inventory in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). +## Payment Module -When a product variant is purchased, the Medusa application decides whether the purchased item requires shipping in the following order: +You can specify for each region which payment providers are available for use. -1. The product variant has an inventory item. In this case, the Medusa application uses the inventory item's `requires_shipping` property to determine if the item requires shipping. -2. If the product variant doesn't have an inventory item, the Medusa application checks whether the product has an associated shipping profile to determine if the item requires shipping. +Medusa defines a module link between the `PaymentProvider` and the `Region` data models. -*** + -## Use Case Examples +### Retrieve with Query -By combining configurations of shipment requirements and inventory management, you can set up your products to support your use case: +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`: -|Use Case|Configurations|Example| -|---|---|---|---|---| -|Item that's shipped on purchase, and its variant inventory is managed by the Medusa application.||Any stock-kept item (clothing, for example), whose inventory is managed in the Medusa application.| -|Item that's shipped on purchase, but its variant inventory is managed externally (not by Medusa) or it has infinite stock.||Any stock-kept item (clothing, for example), whose inventory is managed in an ERP or has infinite stock.| -|Item that's not shipped on purchase, but its variant inventory is managed by Medusa.||Digital products, such as licenses, that don't require shipping but have a limited quantity.| -|Item that doesn't require shipping and its variant inventory isn't managed by Medusa.||| +### query.graph +```ts +const { data: regions } = await query.graph({ + entity: "region", + fields: [ + "payment_providers.*", + ], +}) -# Payment +// regions.payment_providers +``` -In this document, you’ll learn what a payment is and how it's created, captured, and refunded. +### useQueryGraphStep -## What's a Payment? +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -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: +const { data: regions } = useQueryGraphStep({ + entity: "region", + fields: [ + "payment_providers.*", + ], +}) -- 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. +// regions.payment_providers +``` -*** +### Manage with Link -## Capture Payments +To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -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. +### link.create -The payment can also be captured incrementally, each time a capture record is created for that amount. +```ts +import { Modules } from "@medusajs/framework/utils" - +// ... -*** +await link.create({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` -## Refund Payments +### createRemoteLinkStep -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. +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -A payment can be refunded multiple times, and each time a refund record is created. +// ... - +createRemoteLinkStep({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` -# Links between Payment Module and Other Modules +# Links between Sales Channel Module and Other Modules -This document showcases the module links defined between the Payment Module and other commerce modules. +This document showcases the module links defined between the Sales Channel Module and other commerce modules. ## Summary -The Payment Module has the following links to other modules: +The Sales Channel 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). +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). *** -## Cart Module +## API Key Module -The Cart Module provides cart-related features, but not payment processing. +A publishable API key allows you to easily specify the sales channel scope in a client request. -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. +Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. -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 -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`: +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: paymentCollections } = await query.graph({ - entity: "payment_collection", +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", fields: [ - "cart.*", + "publishable_api_keys.*", ], }) -// paymentCollections.cart +// salesChannels.publishable_api_keys ``` ### useQueryGraphStep @@ -22977,19 +24021,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: paymentCollections } = useQueryGraphStep({ - entity: "payment_collection", +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", fields: [ - "cart.*", + "publishable_api_keys.*", ], }) -// paymentCollections.cart +// salesChannels.publishable_api_keys ``` ### Manage with Link -To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +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 @@ -22999,11 +24043,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.CART]: { - cart_id: "cart_123", + [Modules.API_KEY]: { + api_key_id: "apk_123", }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", }, }) ``` @@ -23011,43 +24055,42 @@ await link.create({ ### createRemoteLinkStep ```ts +import { Modules } from "@medusajs/framework/utils" import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", + [Modules.API_KEY]: { + api_key_id: "apk_123", }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", }, }) ``` *** -## 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. +## Cart Module -This link is available starting from Medusa `v2.5.0`. +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. ### Retrieve with Query -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`: +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`: ### query.graph ```ts -const { data: accountHolders } = await query.graph({ - entity: "account_holder", +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", fields: [ - "customer.*", + "carts.*", ], }) -// accountHolders.customer +// salesChannels.carts ``` ### useQueryGraphStep @@ -23057,79 +24100,81 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: accountHolders } = useQueryGraphStep({ - entity: "account_holder", +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", fields: [ - "customer.*", + "carts.*", ], }) -// accountHolders.customer +// salesChannels.carts ``` -### Manage with Link +*** -To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +## Order Module -### link.create +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. -```ts -import { Modules } from "@medusajs/framework/utils" +### 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`: -await link.create({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, +### query.graph + +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "orders.*", + ], }) + +// salesChannels.orders ``` -### createRemoteLinkStep +### useQueryGraphStep ```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -createRemoteLinkStep({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", + fields: [ + "orders.*", + ], }) + +// salesChannels.orders ``` *** -## Order Module +## Product Module -An order's payment details are stored in a payment collection. This also applies for claims and exchanges. +A product has different availability for different sales channels. Medusa defines a link between the `Product` and the `SalesChannel` data models. -So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models. + - +A product can be available in more than one sales channel. You can retrieve only the products of a sales channel. ### Retrieve with Query -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`: +To retrieve the products of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: ### query.graph ```ts -const { data: paymentCollections } = await query.graph({ - entity: "payment_collection", +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", fields: [ - "order.*", + "products.*", ], }) -// paymentCollections.order +// salesChannels.products ``` ### useQueryGraphStep @@ -23139,19 +24184,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: paymentCollections } = useQueryGraphStep({ - entity: "payment_collection", +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", fields: [ - "order.*", + "products.*", ], }) -// paymentCollections.order +// salesChannels.products ``` ### Manage with Link -To manage the payment collections of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -23161,11 +24206,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.PRODUCT]: { + product_id: "prod_123", }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", }, }) ``` @@ -23179,40 +24224,40 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.PRODUCT]: { + product_id: "prod_123", }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", }, }) ``` *** -## Region Module +## Stock Location Module -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. +A stock location is associated with a sales channel. This scopes inventory quantities associated with that stock location by the associated sales channel. - +Medusa defines a link between the `SalesChannel` and `StockLocation` data models. -This increases the flexibility of your store. For example, you only show during checkout the payment providers associated with the cart's region. + ### 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`: +To retrieve the stock locations of a sales channel 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: paymentProviders } = await query.graph({ - entity: "payment_provider", +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", fields: [ - "regions.*", + "stock_locations.*", ], }) -// paymentProviders.regions +// salesChannels.stock_locations ``` ### useQueryGraphStep @@ -23222,19 +24267,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: paymentProviders } = useQueryGraphStep({ - entity: "payment_provider", +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", fields: [ - "regions.*", + "stock_locations.*", ], }) -// paymentProviders.regions +// salesChannels.stock_locations ``` ### 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 stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -23244,11 +24289,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.REGION]: { - region_id: "reg_123", + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", }, }) ``` @@ -23262,514 +24307,557 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.REGION]: { - region_id: "reg_123", + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", }, }) ``` -# Payment Module Options +# Publishable API Keys with Sales Channels -In this document, you'll learn about the options of the Payment Module. +In this document, you’ll learn what publishable API keys are and how to use them with sales channels. -## All Module Options +## Publishable API Keys with Sales Channels -|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|-| +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: -## providers Option +```bash +curl http://localhost:9000/store/products \ + x-publishable-api-key: {your_publishable_api_key} +``` -The `providers` option is an array of payment module providers. +The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used. -When the Medusa application starts, these providers are registered and can be used to process payments. +*** -For example: +## How to Create a Publishable API Key? -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +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). -// ... -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/payment", - options: { - providers: [ - { - resolve: "@medusajs/medusa/payment-stripe", - id: "stripe", - options: { - // ... - }, - }, - ], - }, - }, - ], -}) -``` +# Stock Location Concepts -The `providers` option is an array of objects that accept the following properties: +In this document, you’ll learn about the main concepts in the Stock Location Module. -- `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. +## Stock Location +A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. -# Payment Collection +Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. -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? +## StockLocationAddress -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). +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. -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 Stock Location Module and Other Modules -*** +This document showcases the module links defined between the Stock Location Module and other commerce modules. -## Multiple Payments +## Summary -The payment collection supports multiple payment sessions and payments. +The Stock Location Module has the following links to other modules: -You can use this to accept payments in increments or split payments across payment providers. +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). *** -## Usage with the Cart Module +## Fulfillment Module -The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment. +A fulfillment set can be conditioned to a specific stock location. -During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing. +Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. -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). + - +Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. + -# Accept Payment Flow +### Retrieve with Query -In this document, you’ll learn how to implement an accept-payment flow using workflows or the Payment Module's main service. +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`: -It's highly recommended to use Medusa's workflows to implement this flow. Use the Payment Module's main service for more complex cases. +To retrieve the fulfillment providers, pass `fulfillment_providers.*` in `fields`. -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). +### query.graph -## Flow Overview +```ts +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: [ + "fulfillment_sets.*", + ], +}) - +// stockLocations.fulfillment_sets +``` -*** +### useQueryGraphStep -## 1. Create a Payment Collection +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -A payment collection holds all details related to a resource’s payment operations. So, you start off by creating a payment collection. +// ... -For example: +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: [ + "fulfillment_sets.*", + ], +}) -### Using Workflow +// 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 { createPaymentCollectionForCartWorkflow } from "@medusajs/medusa/core-flows" +import { Modules } from "@medusajs/framework/utils" // ... -await createPaymentCollectionForCartWorkflow(req.scope) - .run({ - input: { - cart_id: "cart_123", - }, - }) +await link.create({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) ``` -### Using Service +### createRemoteLinkStep ```ts -const paymentCollection = - await paymentModuleService.createPaymentCollections({ - currency_code: "usd", - amount: 5000, - }) +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", + }, +}) ``` *** -## 2. Create Payment Sessions +## Inventory Module -The payment collection has one or more payment sessions, each being a payment amount to be authorized by a payment provider. +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. -So, after creating the payment collection, create at least one payment session for a provider. +### Retrieve with Query -For example: +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`: -### Using Workflow +### query.graph ```ts -import { createPaymentSessionsWorkflow } from "@medusajs/medusa/core-flows" - -// ... +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: [ + "inventory_levels.*", + ], +}) -const { result: paymentSesion } = await createPaymentSessionsWorkflow(req.scope) - .run({ - input: { - payment_collection_id: "paycol_123", - provider_id: "stripe", - }, - }) +// stockLocations.inventory_levels ``` -### Using Service +### useQueryGraphStep ```ts -const paymentSession = - await paymentModuleService.createPaymentSession( - paymentCollection.id, - { - provider_id: "stripe", - currency_code: "usd", - amount: 5000, - data: { - // any necessary data for the - // payment provider - }, - } - ) +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: [ + "inventory_levels.*", + ], +}) + +// stockLocations.inventory_levels ``` *** -## 3. Authorize Payment Session +## Sales Channel Module -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. +A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel. -For example: +Medusa defines a link between the `SalesChannel` and `StockLocation` data models. -### Using Step + -```ts -import { authorizePaymentSessionStep } from "@medusajs/medusa/core-flows" +### 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`: -authorizePaymentSessionStep({ - id: "payses_123", - context: {}, +### query.graph + +```ts +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: [ + "sales_channels.*", + ], }) + +// stockLocations.sales_channels ``` -### Using Service +### useQueryGraphStep ```ts -const payment = authorizePaymentSessionStep({ - id: "payses_123", - context: {}, +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: [ + "sales_channels.*", + ], }) + +// stockLocations.sales_channels ``` -When the payment authorization is successful, a payment is created and returned. +### Manage with Link -### Handling Additional Action +To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -If you used the `authorizePaymentSessionStep`, you don't need to implement this logic as it's implemented in the step. +### link.create -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. +```ts +import { Modules } from "@medusajs/framework/utils" -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. +// ... -For example: +await link.create({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, +}) +``` + +### createRemoteLinkStep ```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] +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - if (updatedPaymentSession.status === "requires_more") { - // TODO perform required action - // TODO authorize payment again. - } -} -``` +// ... -*** +createRemoteLinkStep({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, +}) +``` -## 4. Payment Flow Complete -The payment flow is complete once the payment session is authorized and the payment is created. +# Application Method -You can then: +In this document, you'll learn what an application method is. -- 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). +## What is an Application Method? -Some payment providers allow capturing the payment automatically once it’s authorized. In that case, you don’t need to do it manually. +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?| -# Webhook Events +## Target Promotion Rules -In this document, you’ll learn how the Payment Module supports listening to webhook events. +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. -## What's a Webhook Event? +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 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. +In this example, the promotion is only applied on products in the cart having the SKU `SHIRT`. *** -## getWebhookActionAndData Method +## Buy Promotion Rules -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. +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. -Medusa implements a webhook listener route at the `/hooks/payment/[identifier]_[provider]` API route, where: +The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation. -- `[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`. +In this example, the cart must have two products with the SKU `SHIRT` for the promotion to be applied. -Use that webhook listener in your third-party payment provider's configurations. - +# Campaign -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. +In this document, you'll learn about campaigns. -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. +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. -### Actions After Webhook Payment Processing +## What is a Campaign? -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. +A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines promotions under the same conditions, such as start and end dates. + -# Payment Module Provider +*** -In this document, you’ll learn what a payment module provider is. +## Campaign Limits -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. +Each campaign has a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times the promotion can be used. -## What's a Payment Module Provider? +There are two types of budgets: -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. +- `spend`: An amount that, when crossed, the promotion becomes unusable. For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied. +- `usage`: The number of times that a promotion can be used. For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied. -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. -### List of Payment Module Providers +# Promotion Actions -- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md) +In this document, you’ll learn about promotion actions and how they’re computed using the [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). + +## computeActions Method + +The Promotion Module's main service has a [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md) that returns an array of actions to perform on a cart when one or more promotions are applied. + +Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` property indicating the type of action. *** -## System Payment Provider +## Action Types -The Payment Module provides a `system` payment provider that acts as a placeholder payment provider. +### `addItemAdjustment` Action -It doesn’t handle payment processing and delegates that to the merchant. It acts similarly to a cash-on-delivery (COD) payment method. +The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount. -*** +This action has the following format: -## How are Payment Providers Created? +```ts +export interface AddItemAdjustmentAction { + action: "addItemAdjustment" + item_id: string + amount: number + code: string + description?: string +} +``` -A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`. +This action means that a new record should be created of the `LineItemAdjustment` data model in the Cart Module, or `OrderLineItemAdjustment` data model in the Order Module. -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. +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties. -*** +### `removeItemAdjustment` Action -## Configure Payment Providers +The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount. -The Payment Module accepts a `providers` option that allows you to register providers in your application. +The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter. -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). +This action has the following format: -*** +```ts +export interface RemoveItemAdjustmentAction { + action: "removeItemAdjustment" + adjustment_id: string + description?: string + code: string +} +``` -## PaymentProvider Data Model +This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property. -When the Medusa application starts and registers the payment providers, it also creates a record of the `PaymentProvider` data model if none exists. +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties. -This data model is used to reference a payment provider and determine whether it’s installed in the application. +### `addShippingMethodAdjustment` Action +The `addShippingMethodAdjustment` action indicates that an adjustment must be made on a shipping method. For example, make the shipping method free. -# Payment Session +This action has the following format: -In this document, you’ll learn what a payment session is. +```ts +export interface AddShippingMethodAdjustment { + action: "addShippingMethodAdjustment" + shipping_method_id: string + amount: number + code: string + description?: string +} +``` -## What's a Payment Session? +This action means that a new record should be created of the `ShippingMethodAdjustment` data model in the Cart Module, or `OrderShippingMethodAdjustment` data model in the Order Module. -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. +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties. -A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. +### `removeShippingMethodAdjustment` Action - +The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount. -*** +The `computeActions` method accepts any previous shipping method adjustments in the `shipping_methods` property of the second parameter. -## data Property +This action has the following format: -Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data. +```ts +export interface RemoveShippingMethodAdjustment { + action: "removeShippingMethodAdjustment" + adjustment_id: string + code: string +} +``` -For example, the customer's ID in Stripe is stored in the `data` property. +When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property. -*** +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties. -## Payment Session Status +### `campaignBudgetExceeded` Action -The `status` property of a payment session indicates its current status. Its value can be: +When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded. -- `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. +This action has the following format: +```ts +export interface CampaignBudgetExceededAction { + action: "campaignBudgetExceeded" + code: string +} +``` -# Links between Region Module and Other Modules +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties. -This document showcases the module links defined between the Region Module and other commerce modules. -## Summary +# Promotion Concepts -The Region Module has the following links to other modules: +In this document, you’ll learn about the main promotion and rule concepts in the Promotion Module. -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. -- [`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). +## What is a Promotion? -*** +A promotion, represented by the [Promotion data model](https://docs.medusajs.com/references/promotion/models/Promotion/index.html.md), is a discount that can be applied on cart items, shipping methods, or entire orders. -## Cart Module +A promotion has two types: -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. +- `standard`: A standard promotion with rules. +- `buyget`: “A buy X get Y” promotion with rules. -### Retrieve with Query +|\`standard\`|\`buyget\`| +|---|---| +|A coupon code that gives customers 10% off their entire order.|Buy two shirts and get another for free.| +|A coupon code that gives customers $15 off any shirt in their order.|Buy two shirts and get 10% off the entire order.| +|A discount applied automatically for VIP customers that removes 10% off their shipping method’s amount.|Spend $100 and get free shipping.| -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`: +The Medusa Admin UI may not provide a way to create each of these promotion examples. However, they are supported by the Promotion Module and Medusa's workflows and API routes. -### query.graph +*** -```ts -const { data: regions } = await query.graph({ - entity: "region", - fields: [ - "carts.*", - ], -}) +## PromotionRule -// regions.carts -``` +A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](https://docs.medusajs.com/references/promotion/models/PromotionRule/index.html.md). -### useQueryGraphStep +For example, you can create a promotion that only customers of the `VIP` customer group can use. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + -// ... +A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. -const { data: regions } = useQueryGraphStep({ - entity: "region", - fields: [ - "carts.*", - ], -}) +For example, `customer_group_id`. Its value is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values. -// regions.carts -``` +When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself. + +For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value. *** -## Order Module +## Flexible Rules -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. +The `PromotionRule`'s `operator` property adds more flexibility to the rule’s condition rather than simple equality (`eq`). -### Retrieve with Query +For example, to restrict the promotion to only `VIP` and `B2B` customer groups: -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`: +- Add a `PromotionRule` record with its `attribute` property set to `customer_group_id` and `operator` property to `in`. +- Add two `PromotionRuleValue` records associated with the rule: one with the value `VIP` and the other `B2B`. -### query.graph + -```ts -const { data: regions } = await query.graph({ - entity: "region", - fields: [ - "orders.*", - ], -}) +In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion. -// regions.orders -``` -### useQueryGraphStep +# Links between Promotion Module and Other Modules -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +This document showcases the module links defined between the Promotion Module and other commerce modules. -// ... +## Summary -const { data: regions } = useQueryGraphStep({ - entity: "region", - fields: [ - "orders.*", - ], -}) +The Promotion Module has the following links to other modules: -// regions.orders -``` +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). *** -## Payment Module +## Cart Module -You can specify for each region which payment providers are available for use. +A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` and `Promotion` data models. -Medusa defines a module link between the `PaymentProvider` and the `Region` data models. + - +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. ### 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 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`. ### query.graph ```ts -const { data: regions } = await query.graph({ - entity: "region", +const { data: promotions } = await query.graph({ + entity: "promotion", fields: [ - "payment_providers.*", + "carts.*", ], }) -// regions.payment_providers +// promotions.carts ``` ### useQueryGraphStep @@ -23779,19 +24867,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: regions } = useQueryGraphStep({ - entity: "region", +const { data: promotions } = useQueryGraphStep({ + entity: "promotion", fields: [ - "payment_providers.*", + "carts.*", ], }) -// regions.payment_providers +// promotions.carts ``` ### 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 promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -23801,11 +24889,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.REGION]: { - region_id: "reg_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", + [Modules.PROMOTION]: { + promotion_id: "promo_123", }, }) ``` @@ -23819,57 +24907,38 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.REGION]: { - region_id: "reg_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", + [Modules.PROMOTION]: { + promotion_id: "promo_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. +## Order Module -Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. +An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. - + ### 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`: +To retrieve the orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: ### query.graph ```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", +const { data: promotions } = await query.graph({ + entity: "promotion", fields: [ - "publishable_api_keys.*", + "orders.*", ], }) -// salesChannels.publishable_api_keys +// promotions.orders ``` ### useQueryGraphStep @@ -23879,19 +24948,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", +const { data: promotions } = useQueryGraphStep({ + entity: "promotion", fields: [ - "publishable_api_keys.*", + "orders.*", ], }) -// salesChannels.publishable_api_keys +// promotions.orders ``` ### 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): +To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -23901,11 +24970,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.API_KEY]: { - api_key_id: "apk_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", + [Modules.PROMOTION]: { + promotion_id: "promo_123", }, }) ``` @@ -23919,36 +24988,51 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.API_KEY]: { - api_key_id: "apk_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", + [Modules.PROMOTION]: { + promotion_id: "promo_123", }, }) ``` + +# Links between Store Module and Other Modules + +This document showcases the module links defined between the Store Module and other commerce modules. + +## Summary + +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). + *** -## Cart Module +## Currency 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. +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. ### 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 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`: ### query.graph ```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", +const { data: stores } = await query.graph({ + entity: "store", fields: [ - "carts.*", + "supported_currencies.currency.*", ], }) -// salesChannels.carts +// stores.supported_currencies ``` ### useQueryGraphStep @@ -23958,2451 +25042,1906 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", +const { data: stores } = useQueryGraphStep({ + entity: "store", fields: [ - "carts.*", + "supported_currencies.currency.*", ], }) -// salesChannels.carts +// stores.supported_currencies ``` -*** -## Order Module +# User Module Options -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. +In this document, you'll learn about the options of the User Module. -### Retrieve with Query +## Module Options -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`: +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" -### query.graph +// ... -```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "orders.*", +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/user", + options: { + jwt_secret: process.env.JWT_SECRET, + }, + }, ], }) - -// salesChannels.orders ``` -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +|Option|Description|Required| +|---|---|---|---|---| +|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes| -// ... +### Environment Variables -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "orders.*", - ], -}) +Make sure to add the necessary environment variables for the above options in `.env`: -// salesChannels.orders +```bash +JWT_SECRET=supersecret ``` -*** - -## Product Module - -A product has different availability for different sales channels. Medusa defines a link between the `Product` and the `SalesChannel` data models. - +# User Creation Flows -A product can be available in more than one sales channel. You can retrieve only the products of a sales channel. +In this document, learn the different ways to create a user using the User Module. -### Retrieve with Query +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users using the dashboard. -To retrieve the products of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: +## Straightforward User Creation -### query.graph +To create a user, use the [create method of the User Module’s main service](https://docs.medusajs.com/references/user/create/index.html.md): ```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "products.*", - ], +const user = await userModuleService.createUsers({ + email: "user@example.com", }) - -// salesChannels.products ``` -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "products.*", - ], -}) - -// salesChannels.products -``` +You can pair this with the Auth Module to allow the user to authenticate, as explained in a [later section](#create-identity-with-the-auth-module). -### Manage with Link +*** -To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +## Invite Users -### link.create +To create a user, you can create an invite for them using the [createInvites method](https://docs.medusajs.com/references/user/createInvites/index.html.md) of the User Module's main service: ```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, +const invite = await userModuleService.createInvites({ + email: "user@example.com", }) ``` -### createRemoteLinkStep +Later, you can accept the invite and create a new user for them: ```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +const invite = + await userModuleService.validateInviteToken("secret_123") -// ... +await userModuleService.updateInvites({ + id: invite.id, + accepted: true, +}) -createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, +const user = await userModuleService.createUsers({ + email: invite.email, }) ``` -*** - -## Stock Location Module +### Invite Expiry -A stock location is associated with a sales channel. This scopes inventory quantities associated with that stock location by the associated sales channel. +An invite has an expiry date. You can renew the expiry date and refresh the token using the [refreshInviteTokens method](https://docs.medusajs.com/references/user/refreshInviteTokens/index.html.md): -Medusa defines a link between the `SalesChannel` and `StockLocation` data models. +```ts +await userModuleService.refreshInviteTokens(["invite_123"]) +``` - +*** -### Retrieve with Query +## Create Identity with the Auth Module -To retrieve the stock locations of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: +By combining the User and Auth Modules, you can use the Auth Module for authenticating users, and the User Module to manage those users. -### query.graph +So, when a user is authenticated, and you receive the `AuthIdentity` object, you can use it to create a user if it doesn’t exist: ```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "stock_locations.*", - ], +const { success, authIdentity } = + await authModuleService.authenticate("emailpass", { + // ... + }) + +const [, count] = await userModuleService.listAndCountUsers({ + email: authIdentity.entity_id, }) -// salesChannels.stock_locations +if (!count) { + const user = await userModuleService.createUsers({ + email: authIdentity.entity_id, + }) +} ``` -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "stock_locations.*", - ], -}) +# Tax Module Options -// salesChannels.stock_locations -``` +In this document, you'll learn about the options of the Tax Module. -### Manage with Link +## providers -To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +The `providers` option is an array of either tax module providers or path to a file that defines a tax provider. -### link.create +When the Medusa application starts, these providers are registered and can be used to retrieve tax lines. -```ts +```ts title="medusa-config.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", - }, +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/tax", + options: { + providers: [ + { + resolve: "./path/to/my-provider", + id: "my-provider", + options: { + // ... + }, + }, + ], + }, + }, + ], }) ``` -### createRemoteLinkStep +The objects in the array accept the following properties: -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +- `resolve`: A string indicating the package name of the module provider or the path to it. +- `id`: A string indicating the provider's unique name or ID. +- `options`: An optional object of the module provider's options. -// ... -createRemoteLinkStep({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` +# Tax Calculation with the Tax Provider +In this document, you’ll learn how tax lines are calculated and what a tax provider is. -# Publishable API Keys with Sales Channels +## Tax Lines Calculation -In this document, you’ll learn what publishable API keys are and how to use them with sales channels. +Tax lines are calculated and retrieved using the [getTaxLines method of the Tax Module’s main service](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). It accepts an array of line items and shipping methods, and the context of the calculation. -## Publishable API Keys with Sales Channels +For example: -A publishable API key, provided by the API Key Module, is a client key scoped to one or more sales channels. +```ts +const taxLines = await taxModuleService.getTaxLines( + [ + { + id: "cali_123", + product_id: "prod_123", + unit_price: 1000, + quantity: 1, + }, + { + id: "casm_123", + shipping_option_id: "so_123", + unit_price: 2000, + }, + ], + { + address: { + country_code: "us", + }, + } +) +``` -When sending a request to a Store API route, you must pass a publishable API key in the header of the request: +The context object is used to determine which tax regions and rates to use in the calculation. It includes properties related to the address and customer. -```bash -curl http://localhost:9000/store/products \ - x-publishable-api-key: {your_publishable_api_key} -``` +The example above retrieves the tax lines based on the tax region for the United States. -The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used. +The method returns tax lines for the line item and shipping methods. For example: + +```json +[ + { + "line_item_id": "cali_123", + "rate_id": "txr_1", + "rate": 10, + "code": "XXX", + "name": "Tax Rate 1" + }, + { + "shipping_line_id": "casm_123", + "rate_id": "txr_2", + "rate": 5, + "code": "YYY", + "name": "Tax Rate 2" + } +] +``` *** -## How to Create a Publishable API Key? +## Using the Tax Provider in the Calculation -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). +The tax lines retrieved by the `getTaxLines` method are actually retrieved from the tax region’s provider. +A tax module provider whose main service implements the logic to shape tax lines. Each tax region has a tax provider. -# Stock Location Concepts +The Tax Module provides a `system` tax provider that only transforms calculated item and shipping tax rates into the required return type. -In this document, you’ll learn about the main concepts in the Stock Location Module. +{/* --- -## Stock Location +TODO add once tax provider guide is updated + add module providers match other modules. -A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. +## Create Tax Provider -Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. +Refer to [this guide](/modules/tax/provider) to learn more about creating a tax provider. */} -*** -## StockLocationAddress +# Tax Rates and Rules -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. +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. -# Links between Stock Location Module and Other Modules +## What are Tax Rates? -This document showcases the module links defined between the Stock Location Module and other commerce modules. +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. -## Summary +Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. -The Stock Location Module has the following links to other modules: +### Combinable Tax Rates -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +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`. -- [`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). +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. *** -## Fulfillment Module +## Override Tax Rates with Rules -A fulfillment set can be conditioned to a specific stock location. +You can create tax rates that override the default for specific conditions or rules. -Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. +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. -Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. + - +These two properties of the data model identify the rule’s target: -### Retrieve with Query +- `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. -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`: +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. -To retrieve the fulfillment providers, pass `fulfillment_providers.*` in `fields`. -### query.graph +# Tax Region -```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: [ - "fulfillment_sets.*", - ], -}) +In this document, you’ll learn about tax regions and how to use them with the Region Module. -// stockLocations.fulfillment_sets -``` +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. -### useQueryGraphStep +## What is a Tax Region? -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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. -// ... +Tax regions can inherit settings and rules from a parent tax region. -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: [ - "fulfillment_sets.*", - ], -}) +Each tax region has tax rules and a tax provider. -// stockLocations.fulfillment_sets -``` -### Manage with Link +# Emailpass Auth Module Provider -To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +In this document, you’ll learn about the Emailpass auth module provider and how to install and use it in the Auth Module. -### link.create +Using the Emailpass auth module provider, you allow users to register and login with an email and password. -```ts -import { Modules } from "@medusajs/framework/utils" +*** -// ... +## Register the Emailpass Auth Module Provider -await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` +The Emailpass auth provider is registered by default with the Auth Module. -### createRemoteLinkStep +If you want to pass options to the provider, add the provider to the `providers` option of the Auth Module: -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +```ts title="medusa-config.ts" +import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" // ... -createRemoteLinkStep({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/auth", + dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], + options: { + providers: [ + // other providers... + { + resolve: "@medusajs/medusa/auth-emailpass", + id: "emailpass", + options: { + // options... + }, + }, + ], + }, + }, + ], }) ``` +### Module Options + +|Configuration|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`hashConfig\`|An object of configurations to use when hashing the user's +password. Refer to |No|\`\`\`ts +const hashConfig = \{ +  logN: 15, +  r: 8, +  p: 1 +} +\`\`\`| + *** -## Inventory Module +## Related Guides -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. +- [How to register a customer using email and password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md) -### 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`: +# Google Auth Module Provider -### query.graph +In this document, you’ll learn about the Google Auth Module Provider and how to install and use it in the Auth Module. -```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: [ - "inventory_levels.*", - ], -}) +The Google Auth Module Provider authenticates users with their Google account. -// stockLocations.inventory_levels -``` +Learn about the authentication flow for third-party providers in [this guide](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). -### useQueryGraphStep +*** -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +## Register the Google Auth Module Provider + +### Prerequisites + +- [Create a project in Google Cloud.](https://cloud.google.com/resource-manager/docs/creating-managing-projects) +- [Create authorization credentials. When setting the Redirect Uri, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred) + +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" // ... -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: [ - "inventory_levels.*", +module.exports = defineConfig({ + // ... + modules: [ + { + // ... + [Modules.AUTH]: { + resolve: "@medusajs/medusa/auth", + dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], + options: { + providers: [ + // other providers... + { + resolve: "@medusajs/medusa/auth-google", + id: "google", + options: { + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackUrl: process.env.GOOGLE_CALLBACK_URL, + }, + }, + ], + }, + }, + }, ], }) - -// stockLocations.inventory_levels ``` -*** +### Environment Variables -## Sales Channel Module +Make sure to add the necessary environment variables for the above options in `.env`: -A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel. +```plain +GOOGLE_CLIENT_ID=<YOUR_GOOGLE_CLIENT_ID> +GOOGLE_CLIENT_SECRET=<YOUR_GOOGLE_CLIENT_SECRET> +GOOGLE_CALLBACK_URL=<YOUR_GOOGLE_CALLBACK_URL> +``` -Medusa defines a link between the `SalesChannel` and `StockLocation` data models. +### Module Options - +|Configuration|Description|Required| +|---|---|---|---|---| +|\`clientId\`|A string indicating the |Yes| +|\`clientSecret\`|A string indicating the |Yes| +|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in Google.|Yes| -### 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 +## Override Callback URL During Authentication -```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: [ - "sales_channels.*", - ], -}) +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. -// stockLocations.sales_channels -``` +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). -### useQueryGraphStep +*** -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +## Examples -// ... +- [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). -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: [ - "sales_channels.*", - ], -}) -// stockLocations.sales_channels -``` +# GitHub Auth Module Provider -### Manage with Link +In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module. -To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +The Github Auth Module Provider authenticates users with their GitHub account. -### link.create +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). -```ts -import { Modules } from "@medusajs/framework/utils" +*** -// ... +## Register the Github Auth Module Provider -await link.create({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` +### Prerequisites -### createRemoteLinkStep +- [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) -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +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" // ... -createRemoteLinkStep({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, +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 -# Promotion Actions - -In this document, you’ll learn about promotion actions and how they’re computed using the [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). +Make sure to add the necessary environment variables for the above options in `.env`: -## computeActions Method +```plain +GITHUB_CLIENT_ID=<YOUR_GITHUB_CLIENT_ID> +GITHUB_CLIENT_SECRET=<YOUR_GITHUB_CLIENT_SECRET> +GITHUB_CALLBACK_URL=<YOUR_GITHUB_CALLBACK_URL> +``` -The Promotion Module's main service has a [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md) that returns an array of actions to perform on a cart when one or more promotions are applied. +### Module Options -Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` property indicating the type of action. +|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| *** -## Action Types - -### `addItemAdjustment` Action +## Override Callback URL During Authentication -The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount. +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. -This action has the following format: +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). -```ts -export interface AddItemAdjustmentAction { - action: "addItemAdjustment" - item_id: string - amount: number - code: string - description?: string -} -``` +*** -This action means that a new record should be created of the `LineItemAdjustment` data model in the Cart Module, or `OrderLineItemAdjustment` data model in the Order Module. +## Examples -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties. +- [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). -### `removeItemAdjustment` Action -The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount. +# Stripe Module Provider -The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter. +In this document, you’ll learn about the Stripe Module Provider and how to configure it in the Payment Module. -This action has the following format: +Your technical team must install the Stripe Module Provider in your Medusa application first. Then, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to enable the Stripe payment provider in a region using the Medusa Admin dashboard. -```ts -export interface RemoveItemAdjustmentAction { - action: "removeItemAdjustment" - adjustment_id: string - description?: string - code: string -} -``` +## Register the Stripe Module Provider -This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property. +### Prerequisites -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties. +- [Stripe account](https://stripe.com/) +- [Stripe Secret API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) +- [For deployed Medusa applications, a Stripe webhook secret. Refer to the end of this guide for details on the URL and events.](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) -### `addShippingMethodAdjustment` Action +The Stripe Module Provider is installed by default in your application. To use it, add it to the array of providers passed to the Payment Module in `medusa-config.ts`: -The `addShippingMethodAdjustment` action indicates that an adjustment must be made on a shipping method. For example, make the shipping method free. +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" -This action has the following format: +// ... -```ts -export interface AddShippingMethodAdjustment { - action: "addShippingMethodAdjustment" - shipping_method_id: string - amount: number - code: string - description?: string -} +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/payment", + options: { + providers: [ + { + resolve: "@medusajs/medusa/payment-stripe", + id: "stripe", + options: { + apiKey: process.env.STRIPE_API_KEY, + }, + }, + ], + }, + }, + ], +}) ``` -This action means that a new record should be created of the `ShippingMethodAdjustment` data model in the Cart Module, or `OrderShippingMethodAdjustment` data model in the Order Module. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties. - -### `removeShippingMethodAdjustment` Action - -The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount. - -The `computeActions` method accepts any previous shipping method adjustments in the `shipping_methods` property of the second parameter. +### Environment Variables -This action has the following format: +Make sure to add the necessary environment variables for the above options in `.env`: -```ts -export interface RemoveShippingMethodAdjustment { - action: "removeShippingMethodAdjustment" - adjustment_id: string - code: string -} +```bash +STRIPE_API_KEY=<YOUR_STRIPE_API_KEY> ``` -When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties. +### Module Options -### `campaignBudgetExceeded` Action +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`apiKey\`|A string indicating the Stripe Secret API key.|Yes|-| +|\`webhookSecret\`|A string indicating the Stripe webhook secret. This is only useful for deployed Medusa applications.|Yes|-| +|\`capture\`|Whether to automatically capture payment after authorization.|No|\`false\`| +|\`automatic\_payment\_methods\`|A boolean value indicating whether to enable Stripe's automatic payment methods. This is useful if you integrate services like Apple pay or Google pay.|No|\`false\`| +|\`payment\_description\`|A string used as the default description of a payment if none is available in cart.context.payment\_description.|No|-| -When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded. +*** -This action has the following format: +## Enable Stripe Providers in a Region -```ts -export interface CampaignBudgetExceededAction { - action: "campaignBudgetExceeded" - code: string -} -``` +Before customers can use Stripe to complete their purchases, you must enable the Stripe payment provider(s) in the region where you want to offer this payment method. -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties. +Refer to the [user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to edit a region and enable the Stripe payment provider. +*** -# Application Method +## Setup Stripe Webhooks -In this document, you'll learn what an application method is. +For production applications, you must set up webhooks in Stripe that inform Medusa of changes and updates to payments. Refer to [Stripe's documentation](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) on how to setup webhooks. -## What is an Application Method? +### Webhook URL -The [ApplicationMethod data model](https://docs.medusajs.com/references/promotion/models/ApplicationMethod/index.html.md) defines how a promotion is applied: +Medusa has a `{server_url}/hooks/payment/{provider_id}` API route that you can use to register webhooks in Stripe, where: -|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?| +- `{server_url}` is the URL to your deployed Medusa application in server mode. +- `{provider_id}` is the ID of the provider, such as `stripe_stripe` for basic payments. -## Target Promotion Rules +The Stripe Module Provider supports the following payment types, and the webhook endpoint URL is different for each: -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. +|Stripe Payment Type|Webhook Endpoint URL| +|---|---|---| +|Basic Stripe Payment|\`\{server\_url}/hooks/payment/stripe\_stripe\`| +|Bancontact Payments|\`\{server\_url}/hooks/payment/stripe-bancontact\_stripe\`| +|BLIK Payments|\`\{server\_url}/hooks/payment/stripe-blik\_stripe\`| +|giropay Payments|\`\{server\_url}/hooks/payment/stripe-giropay\_stripe\`| +|iDEAL Payments|\`\{server\_url}/hooks/payment/stripe-ideal\_stripe\`| +|Przelewy24 Payments|\`\{server\_url}/hooks/payment/stripe-przelewy24\_stripe\`| +|PromptPay Payments|\`\{server\_url}/hooks/payment/stripe-promptpay\_stripe\`| -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. +### Webhook Events - +When you set up the webhook in Stripe, choose the following events to listen to: -In this example, the promotion is only applied on products in the cart having the SKU `SHIRT`. +- `payment_intent.amount_capturable_updated` +- `payment_intent.succeeded` +- `payment_intent.payment_failed` *** -## Buy Promotion Rules +## Useful Guides -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. +- [Storefront guide: Add Stripe payment method during checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/stripe/index.html.md). +- [Integrate in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter#stripe-integration/index.html.md). +- [Customize Stripe Integration in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/guides/customize-stripe/index.html.md). -The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation. - +# Get Product Variant Prices using Query -In this example, the cart must have two products with the SKU `SHIRT` for the promotion to be applied. +In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). +The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model. -# Campaign +So, to retrieve data across the linked records of the two modules, you use Query. -In this document, you'll learn about campaigns. +## Retrieve All Product Variant Prices -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. +To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`. -## What is a Campaign? +For example: -A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines promotions under the same conditions, such as start and end dates. +```ts highlights={[["6"]]} +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "*", + "variants.*", + "variants.prices.*", + ], + filters: { + id: [ + "prod_123", + ], + }, +}) +``` - +Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). *** -## Campaign Limits - -Each campaign has a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times the promotion can be used. +## Retrieve Calculated Price for a Context -There are two types of budgets: +The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code. -- `spend`: An amount that, when crossed, the promotion becomes unusable. For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied. -- `usage`: The number of times that a promotion can be used. For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied. +Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md). - +To retrieve calculated prices of variants based on a context, retrieve the products using Query and: +- Pass `variants.calculated_price.*` in the `fields` property. +- Pass a `context` property in the object parameter. Its value is an object of objects that sets the context for the retrieved fields. -# Promotion Concepts +For example: -In this document, you’ll learn about the main promotion and rule concepts in the Promotion Module. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. - -## What is a Promotion? - -A promotion, represented by the [Promotion data model](https://docs.medusajs.com/references/promotion/models/Promotion/index.html.md), is a discount that can be applied on cart items, shipping methods, or entire orders. - -A promotion has two types: - -- `standard`: A standard promotion with rules. -- `buyget`: “A buy X get Y” promotion with rules. - -|\`standard\`|\`buyget\`| -|---|---| -|A coupon code that gives customers 10% off their entire order.|Buy two shirts and get another for free.| -|A coupon code that gives customers $15 off any shirt in their order.|Buy two shirts and get 10% off the entire order.| -|A discount applied automatically for VIP customers that removes 10% off their shipping method’s amount.|Spend $100 and get free shipping.| - -The Medusa Admin UI may not provide a way to create each of these promotion examples. However, they are supported by the Promotion Module and Medusa's workflows and API routes. - -*** - -## PromotionRule - -A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](https://docs.medusajs.com/references/promotion/models/PromotionRule/index.html.md). - -For example, you can create a promotion that only customers of the `VIP` customer group can use. - - - -A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. - -For example, `customer_group_id`. Its value is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values. - -When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself. - -For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value. - -*** - -## Flexible Rules - -The `PromotionRule`'s `operator` property adds more flexibility to the rule’s condition rather than simple equality (`eq`). - -For example, to restrict the promotion to only `VIP` and `B2B` customer groups: - -- Add a `PromotionRule` record with its `attribute` property set to `customer_group_id` and `operator` property to `in`. -- Add two `PromotionRuleValue` records associated with the rule: one with the value `VIP` and the other `B2B`. - - - -In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion. - - -# Links between Promotion Module and Other Modules - -This document showcases the module links defined between the Promotion Module and other commerce modules. - -## Summary - -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). - -*** - -## Cart Module - -A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` and `Promotion` data models. - - - -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. - -### 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`. - -### query.graph - -```ts -const { data: promotions } = await query.graph({ - entity: "promotion", - fields: [ - "carts.*", - ], -}) - -// promotions.carts -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]} +import { QueryContext } from "@medusajs/framework/utils" // ... -const { data: promotions } = useQueryGraphStep({ - entity: "promotion", +const { data: products } = await query.graph({ + entity: "product", fields: [ - "carts.*", + "*", + "variants.*", + "variants.calculated_price.*", ], -}) - -// promotions.carts -``` - -### 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): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", + filters: { + id: "prod_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + context: { + variants: { + calculated_price: QueryContext({ + region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS", + currency_code: "eur", + }), + }, }, }) ``` -*** - -## Order Module +For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`. -An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. +`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md). - +Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). -### Retrieve with Query -To retrieve the orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: +# Calculate Product Variant Price with Taxes -### query.graph +In this document, you'll learn how to calculate a product variant's price with taxes. -```ts -const { data: promotions } = await query.graph({ - entity: "promotion", - fields: [ - "orders.*", - ], -}) +## Step 0: Resolve Resources -// promotions.orders -``` +You'll need the following resources for the taxes calculation: -### useQueryGraphStep +1. [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the product's variants' prices for a context. Learn more about that in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md). +2. The Tax Module's main service to get the tax lines for each product. ```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: promotions } = useQueryGraphStep({ - entity: "promotion", - fields: [ - "orders.*", - ], -}) +// other imports... +import { + Modules, + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -// promotions.orders +// In an API route, workflow step, etc... +const query = container.resolve(ContainerRegistrationKeys.QUERY) +const taxModuleService = container.resolve( + Modules.TAX +) ``` -### Manage with Link - -To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" +*** -// ... +## Step 1: Retrieve Prices for a Context -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` +After resolving the resources, use Query to retrieve the products with the variants' prices for a context: -### createRemoteLinkStep +Learn more about retrieving product variants' prices for a context in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md). ```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +import { QueryContext } from "@medusajs/framework/utils" // ... -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "*", + "variants.*", + "variants.calculated_price.*", + ], + filters: { + id: "prod_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + context: { + variants: { + calculated_price: QueryContext({ + region_id: "region_123", + currency_code: "usd", + }), + }, }, }) ``` - -# Links between Store Module and Other Modules - -This document showcases the module links defined between the Store Module and other commerce modules. - -## Summary - -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). - *** -## Currency 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](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. - -### 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`: - -### query.graph - -```ts -const { data: stores } = await query.graph({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) - -// stores.supported_currencies -``` +## Step 2: Get Tax Lines for Products -### useQueryGraphStep +To retrieve the tax line of each product, first, add the following utility method: ```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +// other imports... +import { + HttpTypes, + TaxableItemDTO, +} from "@medusajs/framework/types" // ... +const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => { + return product.variants + ?.map((variant) => { + if (!variant.calculated_price) { + return + } -const { data: stores } = useQueryGraphStep({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) - -// stores.supported_currencies + return { + id: variant.id, + product_id: product.id, + product_name: product.title, + product_categories: product.categories?.map((c) => c.name), + product_category_id: product.categories?.[0]?.id, + product_sku: variant.sku, + product_type: product.type, + product_type_id: product.type_id, + quantity: 1, + unit_price: variant.calculated_price.calculated_amount, + currency_code: variant.calculated_price.currency_code, + } + }) + .filter((v) => !!v) as unknown as TaxableItemDTO[] +} ``` +This formats the products as items to calculate tax lines for. -# Tax Module Options - -In this document, you'll learn about the options of the Tax Module. - -## providers - -The `providers` option is an array of either tax module providers or path to a file that defines a tax provider. - -When the Medusa application starts, these providers are registered and can be used to retrieve tax lines. +Then, use it when retrieving the tax lines of the products retrieved earlier: -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +```ts +// other imports... +import { + ItemTaxLineDTO, +} from "@medusajs/framework/types" // ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/tax", - options: { - providers: [ - { - resolve: "./path/to/my-provider", - id: "my-provider", - options: { - // ... - }, - }, - ], - }, +const taxLines = (await taxModuleService.getTaxLines( + products.map(asTaxItem).flat(), + { + // example of context properties. You can pass other ones. + address: { + country_code, }, - ], -}) + } +)) as unknown as ItemTaxLineDTO[] ``` -The objects in the array accept the following properties: - -- `resolve`: A string indicating the package name of the module provider or the path to it. -- `id`: A string indicating the provider's unique name or ID. -- `options`: An optional object of the module provider's options. - - -# 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. +You use the Tax Module's main service's [getTaxLines method](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md) to retrieve the tax line. -### Combinable Tax Rates +For the first parameter, you use the `asTaxItem` function to format the products as expected by the `getTaxLines` method. -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`. +For the second parameter, you pass the current context. You can pass other details such as the customer's ID. -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. +Learn about the other context properties to pass in [the getTaxLines method's reference](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). *** -## 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. - - - -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 Region - -In this document, you’ll learn about tax regions and how to use them with the Region Module. - -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. - -## 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. +## Step 3: Calculate Price with Tax for Variant -Tax regions can inherit settings and rules from a parent tax region. +To calculate the price with and without taxes for a variant, first, group the tax lines retrieved in the previous step by variant IDs: -Each tax region has tax rules and a tax provider. +```ts highlights={taxLineHighlights} +const taxLinesMap = new Map<string, ItemTaxLineDTO[]>() +taxLines.forEach((taxLine) => { + const variantId = taxLine.line_item_id + if (!taxLinesMap.has(variantId)) { + taxLinesMap.set(variantId, []) + } + taxLinesMap.get(variantId)?.push(taxLine) +}) +``` -# Tax Calculation with the Tax Provider +Notice that the variant's ID is stored in the `line_item_id` property of a tax line since tax lines are used for line items in a cart. -In this document, you’ll learn how tax lines are calculated and what a tax provider is. +Then, loop over the products and their variants to retrieve the prices with and without taxes: -## Tax Lines Calculation +```ts highlights={calculateTaxHighlights} +// other imports... +import { + calculateAmountsWithTax, +} from "@medusajs/framework/utils" -Tax lines are calculated and retrieved using the [getTaxLines method of the Tax Module’s main service](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). It accepts an array of line items and shipping methods, and the context of the calculation. +// ... +products.forEach((product) => { + product.variants?.forEach((variant) => { + if (!variant.calculated_price) { + return + } -For example: + const taxLinesForVariant = taxLinesMap.get(variant.id) || [] + const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({ + taxLines: taxLinesForVariant, + amount: variant.calculated_price!.calculated_amount!, + includesTax: + variant.calculated_price!.is_calculated_price_tax_inclusive!, + }) -```ts -const taxLines = await taxModuleService.getTaxLines( - [ - { - id: "cali_123", - product_id: "prod_123", - unit_price: 1000, - quantity: 1, - }, - { - id: "casm_123", - shipping_option_id: "so_123", - unit_price: 2000, - }, - ], - { - address: { - country_code: "us", - }, - } -) + // do something with prices... + }) +}) ``` -The context object is used to determine which tax regions and rates to use in the calculation. It includes properties related to the address and customer. +For each product variant, you: -The example above retrieves the tax lines based on the tax region for the United States. +1. Retrieve its tax lines from the `taxLinesMap`. +2. Calculate its prices with and without taxes using the `calculateAmountsWithTax` from the Medusa Framework. +3. The `calculateAmountsWithTax` function returns an object having two properties: + - `priceWithTax`: The variant's price with the taxes applied. + - `priceWithoutTax`: The variant's price without taxes applied. -The method returns tax lines for the line item and shipping methods. For example: -```json -[ - { - "line_item_id": "cali_123", - "rate_id": "txr_1", - "rate": 10, - "code": "XXX", - "name": "Tax Rate 1" - }, - { - "shipping_line_id": "casm_123", - "rate_id": "txr_2", - "rate": 5, - "code": "YYY", - "name": "Tax Rate 2" - } -] -``` - -*** - -## Using the Tax Provider in the Calculation - -The tax lines retrieved by the `getTaxLines` method are actually retrieved from the tax region’s provider. - -A tax module provider whose main service implements the logic to shape tax lines. Each tax region has a tax provider. - -The Tax Module provides a `system` tax provider that only transforms calculated item and shipping tax rates into the required return type. - -{/* --- - -TODO add once tax provider guide is updated + add module providers match other modules. - -## Create Tax Provider - -Refer to [this guide](/modules/tax/provider) to learn more about creating a tax provider. */} - - -# User Creation Flows - -In this document, learn the different ways to create a user using the User Module. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users using the dashboard. - -## Straightforward User Creation - -To create a user, use the [create method of the User Module’s main service](https://docs.medusajs.com/references/user/create/index.html.md): - -```ts -const user = await userModuleService.createUsers({ - email: "user@example.com", -}) -``` - -You can pair this with the Auth Module to allow the user to authenticate, as explained in a [later section](#create-identity-with-the-auth-module). - -*** - -## Invite Users - -To create a user, you can create an invite for them using the [createInvites method](https://docs.medusajs.com/references/user/createInvites/index.html.md) of the User Module's main service: - -```ts -const invite = await userModuleService.createInvites({ - email: "user@example.com", -}) -``` - -Later, you can accept the invite and create a new user for them: - -```ts -const invite = - await userModuleService.validateInviteToken("secret_123") - -await userModuleService.updateInvites({ - id: invite.id, - accepted: true, -}) - -const user = await userModuleService.createUsers({ - email: invite.email, -}) -``` - -### Invite Expiry - -An invite has an expiry date. You can renew the expiry date and refresh the token using the [refreshInviteTokens method](https://docs.medusajs.com/references/user/refreshInviteTokens/index.html.md): - -```ts -await userModuleService.refreshInviteTokens(["invite_123"]) -``` - -*** - -## Create Identity with the Auth Module - -By combining the User and Auth Modules, you can use the Auth Module for authenticating users, and the User Module to manage those users. - -So, when a user is authenticated, and you receive the `AuthIdentity` object, you can use it to create a user if it doesn’t exist: - -```ts -const { success, authIdentity } = - await authModuleService.authenticate("emailpass", { - // ... - }) - -const [, count] = await userModuleService.listAndCountUsers({ - email: authIdentity.entity_id, -}) - -if (!count) { - const user = await userModuleService.createUsers({ - email: authIdentity.entity_id, - }) -} -``` - - -# User Module Options +## Workflows -In this document, you'll learn about the options of the User Module. +- [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/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) +- [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/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) +- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/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) +- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md) +- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/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) +- [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md) +- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) +- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) +- [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md) +- [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/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) +- [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) +- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) +- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md) +- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/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) +- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) +- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/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) +- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md) +- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/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) +- [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md) +- [cancelFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelFulfillmentWorkflow/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) +- [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/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) +- [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) +- [updateShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionsWorkflow/index.html.md) +- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md) +- [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/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) +- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md) +- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/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) +- [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md) +- [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/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) +- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) +- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/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) +- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/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) +- [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md) +- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md) +- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md) +- [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/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) +- [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md) +- [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md) +- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/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) +- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md) +- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) +- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md) +- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md) +- [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/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) +- [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md) +- [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md) +- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/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) +- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/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) +- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md) +- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/index.html.md) +- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/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) +- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md) +- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/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) +- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/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) +- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) +- [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md) +- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md) +- [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md) +- [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md) +- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/index.html.md) +- [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md) +- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/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) +- [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/index.html.md) +- [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md) +- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) +- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/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) +- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md) +- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md) +- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/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) +- [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) +- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/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) +- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) +- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md) +- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) +- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) +- [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md) +- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md) +- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) +- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md) +- [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md) +- [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md) +- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md) +- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/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) +- [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/index.html.md) +- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md) +- [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md) +- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md) +- [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md) +- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md) +- [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md) +- [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md) +- [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md) +- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) +- [removeClaimAddItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimAddItemActionValidationStep/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) +- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/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) +- [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md) +- [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md) +- [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md) +- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md) +- [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md) +- [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md) +- [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md) +- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md) +- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/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) +- [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) +- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md) +- [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/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) +- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md) +- [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md) +- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md) +- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md) +- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) +- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md) +- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/index.html.md) +- [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/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) +- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md) +- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md) +- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md) +- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) +- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) +- [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md) +- [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md) +- [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md) +- [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md) +- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/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) +- [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/index.html.md) +- [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md) +- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) +- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) +- [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md) +- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md) +- [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/index.html.md) +- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md) +- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) +- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) +- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/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) +- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/index.html.md) +- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/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) +- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) +- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/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) +- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/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) +- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md) +- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/index.html.md) +- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md) +- [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md) +- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md) +- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) +- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) +- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md) +- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/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) +- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) +- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) +- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md) +- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) +- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) +- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md) +- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) +- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md) +- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md) +- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md) +- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) +- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/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) +- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) +- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md) +- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md) +- [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md) +- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md) +- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md) +- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/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) +- [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/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) +- [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md) +- [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md) +- [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md) +- [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/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) +- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md) +- [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/index.html.md) +- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/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) +- [updateReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnReasonsWorkflow/index.html.md) +- [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/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) +- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md) +- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/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) +- [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) +- [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md) +- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md) +- [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/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) +- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) +- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md) +- [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md) +- [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md) +- [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/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) +- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/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) +- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md) +- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) +- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md) +- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) +- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md) -## Module Options -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +## Steps -// ... +- [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) +- [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) +- [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) +- [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md) +- [validatePresenceOfStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePresenceOfStep/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) +- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/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) +- [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md) +- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/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) +- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md) +- [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md) +- [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/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) +- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/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) +- [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) +- [setShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/setShippingOptionsPricesStep/index.html.md) +- [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/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) +- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md) +- [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/index.html.md) +- [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/index.html.md) +- [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/index.html.md) +- [adjustInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/adjustInventoryLevelsStep/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) +- [deleteInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryLevelsStep/index.html.md) +- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md) +- [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/index.html.md) +- [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md) +- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md) +- [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/index.html.md) +- [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md) +- [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/index.html.md) +- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md) +- [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/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) +- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/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) +- [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md) +- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md) +- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/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) +- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md) +- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) +- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/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) +- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/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) +- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md) +- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md) +- [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md) +- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md) +- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md) +- [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md) +- [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/index.html.md) +- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/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) +- [updateLineItemsStepWithSelector](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStepWithSelector/index.html.md) +- [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/index.html.md) +- [deleteLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteLineItemsStep/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) +- [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) +- [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) +- [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md) +- [cancelOrderExchangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderExchangeStep/index.html.md) +- [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md) +- [cancelOrderClaimStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderClaimStep/index.html.md) +- [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/index.html.md) +- [createOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderChangeStep/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) +- [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/index.html.md) +- [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/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) +- [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) +- [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) +- [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/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) +- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md) +- [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/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) +- [registerOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderFulfillmentStep/index.html.md) +- [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md) +- [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/index.html.md) +- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md) +- [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md) +- [updateOrderShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsStep/index.html.md) +- [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/index.html.md) +- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) +- [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/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) +- [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) +- [createRefundReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRefundReasonStep/index.html.md) +- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md) +- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md) +- [validateDeletedPaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDeletedPaymentSessionsStep/index.html.md) +- [createPriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListPricesStep/index.html.md) +- [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/index.html.md) +- [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/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) +- [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md) +- [createPricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPricePreferencesStep/index.html.md) +- [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md) +- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/index.html.md) +- [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/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) +- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/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) +- [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/index.html.md) +- [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/index.html.md) +- [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md) +- [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/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) +- [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/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) +- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md) +- [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md) +- [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md) +- [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/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) +- [groupProductsForBatchStep](https://docs.medusajs.com/references/medusa-workflows/steps/groupProductsForBatchStep/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) +- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/index.html.md) +- [deleteProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductCategoriesStep/index.html.md) +- [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/index.html.md) +- [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md) +- [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) +- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md) +- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md) +- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/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) +- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md) +- [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/index.html.md) +- [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md) +- [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/index.html.md) +- [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/index.html.md) +- [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md) +- [updatePromotionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionRulesStep/index.html.md) +- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md) +- [createReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnReasonsStep/index.html.md) +- [deleteReturnReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnReasonStep/index.html.md) +- [updateReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnReasonsStep/index.html.md) +- [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/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) +- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md) +- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/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) +- [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) +- [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/index.html.md) +- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/index.html.md) +- [updateStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStockLocationsStep/index.html.md) +- [updateStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStoresStep/index.html.md) +- [deleteStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStoresStep/index.html.md) +- [createStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/createStoresStep/index.html.md) +- [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/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) +- [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md) +- [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md) +- [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/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) +- [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) +- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/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) -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/user", - options: { - jwt_secret: process.env.JWT_SECRET, - }, - }, - ], -}) -``` -|Option|Description|Required| -|---|---|---|---|---| -|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes| +# Medusa CLI Reference -### Environment Variables +The Medusa CLI tool provides commands that facilitate your development. -Make sure to add the necessary environment variables for the above options in `.env`: +### Prerequisites -```bash -JWT_SECRET=supersecret -``` +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) +## Usage -# Emailpass Auth Module Provider +In your Medusa application's directory, you can use the Medusa CLI tool using NPX. -In this document, you’ll learn about the Emailpass auth module provider and how to install and use it in the Auth Module. +For example: -Using the Emailpass auth module provider, you allow users to register and login with an email and password. +```bash +npx medusa --help +``` *** -## Register the Emailpass Auth Module Provider -The Emailpass auth provider is registered by default with the Auth Module. +# db Commands - Medusa CLI Reference -If you want to pass options to the provider, add the provider to the `providers` option of the Auth Module: +Commands starting with `db:` perform actions on the database. -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" +## db:setup -// ... +Creates a database for the Medusa application with the specified name, if it doesn't exit. Then, it runs migrations and syncs links. -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/auth", - dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], - options: { - providers: [ - // other providers... - { - resolve: "@medusajs/medusa/auth-emailpass", - id: "emailpass", - options: { - // options... - }, - }, - ], - }, - }, - ], -}) +It also updates your `.env` file with the database name. + +```bash +npx medusa db:setup --db <name> ``` -### Module Options +Use this command if you're setting up a Medusa project or database manually. -|Configuration|Description|Required|Default| +### Options + +|Option|Description|Required|Default| |---|---|---|---|---|---|---| -|\`hashConfig\`|An object of configurations to use when hashing the user's -password. Refer to |No|\`\`\`ts -const hashConfig = \{ -  logN: 15, -  r: 8, -  p: 1 -} -\`\`\`| +|\`--db \<name>\`|The database name.|Yes|-| +|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.| +|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| +|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| +|\`--no-interactive\`|Disable the command's prompts.|No|-| *** -## Related Guides +## db:create -- [How to register a customer using email and password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md) +Creates a database for the Medusa application with the specified name, if it doesn't exit. +It also updates your `.env` file with the database name. -# Google Auth Module Provider +```bash +npx medusa db:create --db <name> +``` -In this document, you’ll learn about the Google Auth Module Provider and how to install and use it in the Auth Module. +Use this command if you want to only create a database. -The Google Auth Module Provider authenticates users with their Google account. +### Options -Learn about the authentication flow for third-party providers in [this guide](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). +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`--db \<name>\`|The database name.|Yes|-| +|\`--no-interactive\`|Disable the command's prompts.|No|-| *** -## Register the Google Auth Module Provider - -### Prerequisites - -- [Create a project in Google Cloud.](https://cloud.google.com/resource-manager/docs/creating-managing-projects) -- [Create authorization credentials. When setting the Redirect Uri, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred) - -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: [ - { - // ... - [Modules.AUTH]: { - resolve: "@medusajs/medusa/auth", - dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], - options: { - providers: [ - // other providers... - { - resolve: "@medusajs/medusa/auth-google", - id: "google", - options: { - clientId: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackUrl: process.env.GOOGLE_CALLBACK_URL, - }, - }, - ], - }, - }, - }, - ], -}) -``` - -### Environment Variables +## db:generate -Make sure to add the necessary environment variables for the above options in `.env`: +Generate a migration file for the latest changes in one or more modules. -```plain -GOOGLE_CLIENT_ID=<YOUR_GOOGLE_CLIENT_ID> -GOOGLE_CLIENT_SECRET=<YOUR_GOOGLE_CLIENT_SECRET> -GOOGLE_CALLBACK_URL=<YOUR_GOOGLE_CALLBACK_URL> +```bash +npx medusa db:generate <module_names...> ``` -### Module Options +### Arguments -|Configuration|Description|Required| +|Argument|Description|Required| |---|---|---|---|---| -|\`clientId\`|A string indicating the |Yes| -|\`clientSecret\`|A string indicating the |Yes| -|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in Google.|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). +|\`module\_names\`|The name of one or more module (separated by spaces) to generate migrations for. For example, |Yes| *** -## Examples - -- [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). +## db:migrate +Run the latest migrations to reflect changes on the database, sync link definitions with the database, and run migration data scripts. -# GitHub Auth Module Provider +```bash +npx medusa db:migrate +``` -In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module. +Use this command if you've updated the Medusa packages, or you've created customizations and want to reflect them in the database. -The Github Auth Module Provider authenticates users with their GitHub account. +### Options -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). +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.| +|\`--skip-scripts\`|Skip running data migration scripts. This option is added starting from +|No|Data migration scripts are run by default starting from +| +|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| +|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| *** -## 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 +## db:rollback -Make sure to add the necessary environment variables for the above options in `.env`: +Revert the last migrations ran on one or more modules. -```plain -GITHUB_CLIENT_ID=<YOUR_GITHUB_CLIENT_ID> -GITHUB_CLIENT_SECRET=<YOUR_GITHUB_CLIENT_SECRET> -GITHUB_CALLBACK_URL=<YOUR_GITHUB_CALLBACK_URL> +```bash +npx medusa db:rollback <module_names...> ``` -### Module Options +### Arguments -|Configuration|Description|Required| +|Argument|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). +|\`module\_names\`|The name of one or more module (separated by spaces) to rollback their migrations for. For example, |Yes| *** -## 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). - - -# Get Product Variant Prices using Query - -In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). - -The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model. - -So, to retrieve data across the linked records of the two modules, you use Query. - -## Retrieve All Product Variant Prices - -To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`. +## db:sync-links -For example: +Sync the database with the link definitions in your application, including the definitions in Medusa's modules. -```ts highlights={[["6"]]} -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "*", - "variants.*", - "variants.prices.*", - ], - filters: { - id: [ - "prod_123", - ], - }, -}) +```bash +npx medusa db:sync-links ``` -Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). - -*** - -## Retrieve Calculated Price for a Context - -The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code. - -Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md). - -To retrieve calculated prices of variants based on a context, retrieve the products using Query and: +### Options -- Pass `variants.calculated_price.*` in the `fields` property. -- Pass a `context` property in the object parameter. Its value is an object of objects that sets the context for the retrieved fields. +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`--execute-safe\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| +|\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| -For example: -```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]} -import { QueryContext } from "@medusajs/framework/utils" +# 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. -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "*", - "variants.*", - "variants.calculated_price.*", - ], - filters: { - id: "prod_123", - }, - context: { - variants: { - calculated_price: QueryContext({ - region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS", - currency_code: "eur", - }), - }, - }, -}) +```bash +npx medusa develop ``` -For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`. - -`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md). - -Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). +## Options +|Option|Description|Default| +|---|---|---|---|---| +|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| -# Calculate Product Variant Price with Taxes -In this document, you'll learn how to calculate a product variant's price with taxes. +# build Command - Medusa CLI Reference -## Step 0: Resolve Resources +Create a standalone build of the Medusa application. -You'll need the following resources for the taxes calculation: +This creates a build that: -1. [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the product's variants' prices for a context. Learn more about that in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md). -2. The Tax Module's main service to get the tax lines for each product. +- Doesn't rely on the source TypeScript files. +- Can be copied to a production server reliably. -```ts -// other imports... -import { - Modules, - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +The build is outputted to a new `.medusa/server` directory. -// In an API route, workflow step, etc... -const query = container.resolve(ContainerRegistrationKeys.QUERY) -const taxModuleService = container.resolve( - Modules.TAX -) +```bash +npx medusa build ``` -*** - -## Step 1: Retrieve Prices for a Context - -After resolving the resources, use Query to retrieve the products with the variants' prices for a context: - -Learn more about retrieving product variants' prices for a context in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md). - -```ts -import { QueryContext } from "@medusajs/framework/utils" +Refer to [this section](#run-built-medusa-application) for next steps. -// ... +## Options -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "*", - "variants.*", - "variants.calculated_price.*", - ], - filters: { - id: "prod_123", - }, - context: { - variants: { - calculated_price: QueryContext({ - region_id: "region_123", - currency_code: "usd", - }), - }, - }, -}) -``` +|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 | *** -## Step 2: Get Tax Lines for Products - -To retrieve the tax line of each product, first, add the following utility method: +## Run Built Medusa Application -```ts -// other imports... -import { - HttpTypes, - TaxableItemDTO, -} from "@medusajs/framework/types" +After running the `build` command, use the following step to run the built Medusa application: -// ... -const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => { - return product.variants - ?.map((variant) => { - if (!variant.calculated_price) { - return - } +- Change to the `.medusa/server` directory and install the dependencies: - return { - id: variant.id, - product_id: product.id, - product_name: product.title, - product_categories: product.categories?.map((c) => c.name), - product_category_id: product.categories?.[0]?.id, - product_sku: variant.sku, - product_type: product.type, - product_type_id: product.type_id, - quantity: 1, - unit_price: variant.calculated_price.calculated_amount, - currency_code: variant.calculated_price.currency_code, - } - }) - .filter((v) => !!v) as unknown as TaxableItemDTO[] -} +```bash npm2yarn +cd .medusa/server && npm install ``` -This formats the products as items to calculate tax lines for. - -Then, use it when retrieving the tax lines of the products retrieved earlier: - -```ts -// other imports... -import { - ItemTaxLineDTO, -} from "@medusajs/framework/types" +- 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. -// ... -const taxLines = (await taxModuleService.getTaxLines( - products.map(asTaxItem).flat(), - { - // example of context properties. You can pass other ones. - address: { - country_code, - }, - } -)) as unknown as ItemTaxLineDTO[] +```bash npm2yarn +cp .env .medusa/server/.env.production ``` -You use the Tax Module's main service's [getTaxLines method](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md) to retrieve the tax line. - -For the first parameter, you use the `asTaxItem` function to format the products as expected by the `getTaxLines` method. - -For the second parameter, you pass the current context. You can pass other details such as the customer's ID. - -Learn about the other context properties to pass in [the getTaxLines method's reference](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). - -*** - -## Step 3: Calculate Price with Tax for Variant +- In the system environment variables, set `NODE_ENV` to `production`: -To calculate the price with and without taxes for a variant, first, group the tax lines retrieved in the previous step by variant IDs: +```bash +NODE_ENV=production +``` -```ts highlights={taxLineHighlights} -const taxLinesMap = new Map<string, ItemTaxLineDTO[]>() -taxLines.forEach((taxLine) => { - const variantId = taxLine.line_item_id - if (!taxLinesMap.has(variantId)) { - taxLinesMap.set(variantId, []) - } +- Use the `start` command to run the application: - taxLinesMap.get(variantId)?.push(taxLine) -}) +```bash npm2yarn +cd .medusa/server && npm run start ``` -Notice that the variant's ID is stored in the `line_item_id` property of a tax line since tax lines are used for line items in a cart. - -Then, loop over the products and their variants to retrieve the prices with and without taxes: +*** -```ts highlights={calculateTaxHighlights} -// other imports... -import { - calculateAmountsWithTax, -} from "@medusajs/framework/utils" +## Build Medusa Admin -// ... -products.forEach((product) => { - product.variants?.forEach((variant) => { - if (!variant.calculated_price) { - return - } +By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory. - const taxLinesForVariant = taxLinesMap.get(variant.id) || [] - const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({ - taxLines: taxLinesForVariant, - amount: variant.calculated_price!.calculated_amount!, - includesTax: - variant.calculated_price!.is_calculated_price_tax_inclusive!, - }) +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. - // do something with prices... - }) -}) -``` -For each product variant, you: +# new Command - Medusa CLI Reference -1. Retrieve its tax lines from the `taxLinesMap`. -2. Calculate its prices with and without taxes using the `calculateAmountsWithTax` from the Medusa Framework. -3. The `calculateAmountsWithTax` function returns an object having two properties: - - `priceWithTax`: The variant's price with the taxes applied. - - `priceWithoutTax`: The variant's price without taxes applied. +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 [<dir_name> [<starter_url>]] +``` -# Stripe Module Provider +## Arguments -In this document, you’ll learn about the Stripe Module Provider and how to configure it in the Payment Module. +|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\`| -Your technical team must install the Stripe Module Provider in your Medusa application first. Then, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to enable the Stripe payment provider in a region using the Medusa Admin dashboard. +## Options -## Register the Stripe Module Provider +|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 \<user>\`|The database user to use for database setup.| +|\`--db-database \<database>\`|The name of the database used for database setup.| +|\`--db-pass \<password>\`|The database password to use for database setup.| +|\`--db-port \<port>\`|The database port to use for database setup.| +|\`--db-host \<host>\`|The database host to use for database setup.| -### Prerequisites -- [Stripe account](https://stripe.com/) -- [Stripe Secret API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) -- [For deployed Medusa applications, a Stripe webhook secret. Refer to the end of this guide for details on the URL and events.](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) +# plugin Commands - Medusa CLI Reference -The Stripe Module Provider is installed by default in your application. To use it, add it to the array of providers passed to the Payment Module in `medusa-config.ts`: +Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development. -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). -// ... +## plugin:publish -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/payment", - options: { - providers: [ - { - resolve: "@medusajs/medusa/payment-stripe", - id: "stripe", - options: { - apiKey: process.env.STRIPE_API_KEY, - }, - }, - ], - }, - }, - ], -}) +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 ``` -### Environment Variables +*** -Make sure to add the necessary environment variables for the above options in `.env`: +## 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 -STRIPE_API_KEY=<YOUR_STRIPE_API_KEY> +npx medusa plugin:add [names...] ``` -### Module Options +### Arguments -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`apiKey\`|A string indicating the Stripe Secret API key.|Yes|-| -|\`webhookSecret\`|A string indicating the Stripe webhook secret. This is only useful for deployed Medusa applications.|Yes|-| -|\`capture\`|Whether to automatically capture payment after authorization.|No|\`false\`| -|\`automatic\_payment\_methods\`|A boolean value indicating whether to enable Stripe's automatic payment methods. This is useful if you integrate services like Apple pay or Google pay.|No|\`false\`| -|\`payment\_description\`|A string used as the default description of a payment if none is available in cart.context.payment\_description.|No|-| +|Argument|Description|Required| +|---|---|---|---|---| +|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes| *** -## Enable Stripe Providers in a Region +## plugin:develop -Before customers can use Stripe to complete their purchases, you must enable the Stripe payment provider(s) in the region where you want to offer this payment method. +Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry. -Refer to the [user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to edit a region and enable the Stripe payment provider. +```bash +npx medusa plugin:develop +``` *** -## Setup Stripe Webhooks +## plugin:db:generate -For production applications, you must set up webhooks in Stripe that inform Medusa of changes and updates to payments. Refer to [Stripe's documentation](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) on how to setup webhooks. +Generate migrations for all modules in a plugin. -### Webhook URL +```bash +npx medusa plugin:db:generate +``` -Medusa has a `{server_url}/hooks/payment/{provider_id}` API route that you can use to register webhooks in Stripe, where: +*** -- `{server_url}` is the URL to your deployed Medusa application in server mode. -- `{provider_id}` is the ID of the provider, such as `stripe_stripe` for basic payments. +## plugin:build -The Stripe Module Provider supports the following payment types, and the webhook endpoint URL is different for each: +Build a plugin before publishing it to NPM. The command will compile an output in the `.medusa/server` directory. -|Stripe Payment Type|Webhook Endpoint URL| -|---|---|---| -|Basic Stripe Payment|\`\{server\_url}/hooks/payment/stripe\_stripe\`| -|Bancontact Payments|\`\{server\_url}/hooks/payment/stripe-bancontact\_stripe\`| -|BLIK Payments|\`\{server\_url}/hooks/payment/stripe-blik\_stripe\`| -|giropay Payments|\`\{server\_url}/hooks/payment/stripe-giropay\_stripe\`| -|iDEAL Payments|\`\{server\_url}/hooks/payment/stripe-ideal\_stripe\`| -|Przelewy24 Payments|\`\{server\_url}/hooks/payment/stripe-przelewy24\_stripe\`| -|PromptPay Payments|\`\{server\_url}/hooks/payment/stripe-promptpay\_stripe\`| +```bash +npx medusa plugin:build +``` -### Webhook Events -When you set up the webhook in Stripe, choose the following events to listen to: +# exec Command - Medusa CLI Reference -- `payment_intent.amount_capturable_updated` -- `payment_intent.succeeded` -- `payment_intent.payment_failed` +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...] +``` -## Useful Guides +## Arguments -- [Storefront guide: Add Stripe payment method during checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/stripe/index.html.md). -- [Integrate in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter#stripe-integration/index.html.md). -- [Customize Stripe Integration in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/guides/customize-stripe/index.html.md). +|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| -## Workflows +# 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). + +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-cluster +``` + +#### Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`-c \<number>\`|The number of CPUs that Medusa can consume.|Medusa will try to consume all CPUs.| +|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| -- [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) -- [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/index.html.md) -- [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/index.html.md) -- [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/index.html.md) -- [updateApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateApiKeysWorkflow/index.html.md) -- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) -- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) -- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md) -- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md) -- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) -- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md) -- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md) -- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) -- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md) -- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/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) -- [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md) -- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) -- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md) -- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) -- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/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) -- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) -- [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md) -- [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md) -- [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md) -- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) -- [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/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) -- [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md) -- [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/index.html.md) -- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md) -- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md) -- [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/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) -- [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) -- [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/index.html.md) -- [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md) -- [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/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) -- [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) -- [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) -- [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) -- [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md) -- [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md) -- [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md) -- [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/index.html.md) -- [batchInventoryItemLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchInventoryItemLevelsWorkflow/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) -- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) -- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md) -- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md) -- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) -- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) -- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/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) -- [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) -- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/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) -- [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/index.html.md) -- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md) -- [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/index.html.md) -- [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/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) -- [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/index.html.md) -- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md) -- [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md) -- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md) -- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/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) -- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md) -- [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/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) -- [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md) -- [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md) -- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md) -- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/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) -- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md) -- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) -- [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md) -- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) -- [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md) -- [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md) -- [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md) -- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/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) -- [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) -- [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/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) -- [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md) -- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md) -- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) -- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md) -- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/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) -- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md) -- [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md) -- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) -- [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md) -- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/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) -- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md) -- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/index.html.md) -- [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/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) -- [createOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodValidationStep/index.html.md) -- [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/index.html.md) -- [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md) -- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) -- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/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) -- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md) -- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/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) -- [declineTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/declineTransferOrderRequestValidationStep/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) -- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/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) -- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md) -- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) -- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md) -- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) -- [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md) -- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) -- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) -- [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md) -- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md) -- [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md) -- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md) -- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md) -- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/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) -- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md) -- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md) -- [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md) -- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md) -- [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md) -- [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md) -- [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md) -- [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) -- [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md) -- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) -- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md) -- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md) -- [removeClaimAddItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimAddItemActionValidationStep/index.html.md) -- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md) -- [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md) -- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md) -- [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md) -- [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md) -- [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md) -- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md) -- [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md) -- [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md) -- [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md) -- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md) -- [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md) -- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md) -- [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md) -- [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md) -- [removeReturnItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnItemActionValidationStep/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) -- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md) -- [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/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) -- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md) -- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md) -- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) -- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/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) -- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md) -- [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md) -- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) -- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) -- [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md) -- [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md) -- [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md) -- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/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) -- [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/index.html.md) -- [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md) -- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/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) -- [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md) -- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/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) -- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) -- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/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) -- [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) -- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/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) -- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md) -- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/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) -- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) -- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) -- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) -- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md) -- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/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) -- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) -- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md) -- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md) -- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md) -- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) -- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md) -- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md) -- [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md) -- [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/index.html.md) -- [createCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCampaignsWorkflow/index.html.md) -- [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md) -- [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/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) -- [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/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) -- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md) -- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md) -- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) -- [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/index.html.md) -- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) -- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) -- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/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) -- [deleteReservationsByLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsByLineItemsWorkflow/index.html.md) -- [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/index.html.md) -- [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md) -- [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/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) -- [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md) -- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/index.html.md) -- [updateReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnReasonsWorkflow/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) -- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md) -- [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/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) -- [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/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) -- [createStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStoresWorkflow/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) -- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md) -- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md) -- [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md) -- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) -- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md) -- [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/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) -- [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/index.html.md) -- [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md) -- [setTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/setTaxRateRulesWorkflow/index.html.md) +# start Command - Medusa CLI Reference -## Steps +Start the Medusa application in production. -- [createApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/createApiKeysStep/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) -- [linkSalesChannelsToApiKeyStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkSalesChannelsToApiKeyStep/index.html.md) -- [validateSalesChannelsExistStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateSalesChannelsExistStep/index.html.md) -- [updateApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateApiKeysStep/index.html.md) -- [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md) -- [emitEventStep](https://docs.medusajs.com/references/medusa-workflows/steps/emitEventStep/index.html.md) -- [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/index.html.md) -- [updateRemoteLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRemoteLinksStep/index.html.md) -- [dismissRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/dismissRemoteLinkStep/index.html.md) -- [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/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) -- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/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) -- [createCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerAddressesStep/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) -- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/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) -- [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md) -- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md) -- [createCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerGroupsStep/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) -- [deleteCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerGroupStep/index.html.md) -- [updateCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerGroupsStep/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) -- [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) -- [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) -- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/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) -- [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) -- [setShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/setShippingOptionsPricesStep/index.html.md) -- [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/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) -- [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/index.html.md) -- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md) -- [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/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) -- [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/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) -- [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) -- [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/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) -- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/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) -- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md) -- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/index.html.md) -- [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md) -- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) -- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/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) -- [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) -- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/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) -- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md) -- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md) -- [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md) -- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/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) -- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/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) -- [attachInventoryItemToVariants](https://docs.medusajs.com/references/medusa-workflows/steps/attachInventoryItemToVariants/index.html.md) -- [adjustInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/adjustInventoryLevelsStep/index.html.md) -- [createInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryLevelsStep/index.html.md) -- [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/index.html.md) -- [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/index.html.md) -- [deleteInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryLevelsStep/index.html.md) -- [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md) -- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md) -- [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/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) -- [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) -- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/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) -- [addOrderTransactionStep](https://docs.medusajs.com/references/medusa-workflows/steps/addOrderTransactionStep/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) -- [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) -- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md) -- [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/index.html.md) -- [createOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderChangeStep/index.html.md) -- [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/index.html.md) -- [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/index.html.md) -- [createOrderExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangesStep/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) -- [createOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrdersStep/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) -- [deleteOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangeActionsStep/index.html.md) -- [deleteExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteExchangesStep/index.html.md) -- [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/index.html.md) -- [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md) -- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md) -- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md) -- [registerOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderFulfillmentStep/index.html.md) -- [previewOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/previewOrderChangeStep/index.html.md) -- [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/index.html.md) -- [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md) -- [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/index.html.md) -- [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md) -- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md) -- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) -- [updateOrderShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsStep/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) -- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md) -- [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md) -- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/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) -- [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/index.html.md) -- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/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) -- [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/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) -- [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/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) -- [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md) -- [createPricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPricePreferencesStep/index.html.md) -- [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md) -- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/index.html.md) -- [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/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) -- [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) -- [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/index.html.md) -- [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md) -- [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/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) -- [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/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) -- [generateProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/generateProductCsvStep/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) -- [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md) -- [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md) -- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/index.html.md) -- [groupProductsForBatchStep](https://docs.medusajs.com/references/medusa-workflows/steps/groupProductsForBatchStep/index.html.md) -- [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/index.html.md) -- [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/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) -- [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/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) -- [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) -- [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md) -- [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/index.html.md) -- [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md) -- [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/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) -- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md) -- [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/index.html.md) -- [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md) -- [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/index.html.md) -- [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/index.html.md) -- [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md) -- [updatePromotionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionRulesStep/index.html.md) -- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/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) -- [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md) -- [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/index.html.md) -- [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/index.html.md) -- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md) -- [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/index.html.md) -- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/index.html.md) -- [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/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) -- [createReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnReasonsStep/index.html.md) -- [deleteReturnReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnReasonStep/index.html.md) -- [updateReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnReasonsStep/index.html.md) -- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md) -- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/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) -- [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) -- [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/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) -- [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md) -- [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md) -- [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/index.html.md) -- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/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) -- [listTaxRateIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateIdsStep/index.html.md) -- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/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) -- [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) -- [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) -- [deleteStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStoresStep/index.html.md) +```bash +npx medusa start +``` + +## Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \<port>\`|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. + +```bash +npx medusa user --email <email> [--password <password>] +``` + +## Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`-e \<email>\`|The user's email.|Yes|-| +|\`-p \<password>\`|The user's password.|No|-| +|\`-i \<id>\`|The user's ID.|No|An automatically generated ID.| +|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password. +If ran successfully, you'll receive the invite token in the output.|No|\`false\`| # Medusa CLI Reference @@ -26610,6 +27149,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.| +# 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 \<host>\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \<port>\`|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). @@ -26626,20 +27181,33 @@ npx medusa exec [file] [args...] |\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| -# develop Command - Medusa CLI Reference +# new 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. +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 -npx medusa develop +medusa new [<dir_name> [<starter_url>]] ``` +## 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|Default| -|---|---|---|---|---| -|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| +|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 \<user>\`|The database user to use for database setup.| +|\`--db-database \<database>\`|The name of the database used for database setup.| +|\`--db-pass \<password>\`|The database password to use for database setup.| +|\`--db-port \<port>\`|The database port to use for database setup.| +|\`--db-host \<host>\`|The database host to use for database setup.| # plugin Commands - Medusa CLI Reference @@ -26703,35 +27271,6 @@ npx medusa plugin:build ``` -# 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 [<dir_name> [<starter_url>]] -``` - -## 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 \<user>\`|The database user to use for database setup.| -|\`--db-database \<database>\`|The name of the database used for database setup.| -|\`--db-pass \<password>\`|The database password to use for database setup.| -|\`--db-port \<port>\`|The database port to use for database setup.| -|\`--db-host \<host>\`|The database host to use for database setup.| - - # start Command - Medusa CLI Reference Start the Medusa application in production. @@ -26767,6 +27306,22 @@ npx medusa start-cluster |\`-p \<port>\`|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. @@ -26786,2029 +27341,2828 @@ npx medusa user --email <email> [--password <password>] If ran successfully, you'll receive the invite token in the output.|No|\`false\`| -# telemetry Command - Medusa CLI Reference +# Medusa JS SDK -Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. +In this documentation, you'll learn how to install and use Medusa's JS SDK. -```bash -npx medusa telemetry +## What is Medusa JS SDK? + +Medusa's JS SDK is a library to easily send requests to your Medusa application. You can use it in your admin customizations or custom storefronts. + +*** + +## How to Install Medusa JS SDK? + +The Medusa JS SDK is available in your Medusa application by default. So, you don't need to install it before using it in your admin customizations. + +To install the Medusa JS SDK in other projects, such as a custom storefront, run the following command: + +```bash npm2yarn +npm install @medusajs/js-sdk@latest @medusajs/types@latest ``` -#### Options +You install two libraries: -|Option|Description| -|---|---|---| -|\`--enable\`|Enable telemetry (default).| -|\`--disable\`|Disable telemetry.| +- `@medusajs/js-sdk`: the Medusa JS SDK. +- `@medusajs/types`: Medusa's types library, which is useful if you're using TypeScript in your development. +*** -# Medusa CLI Reference +## Setup JS SDK -The Medusa CLI tool provides commands that facilitate your development. +In your project, create the following `config.ts` file: -### Prerequisites +For admin customizations, create this file at `src/admin/lib/config.ts`. -- [Node.js v20+](https://nodejs.org/en/download) -- [Git CLI tool](https://git-scm.com/downloads) -- [PostgreSQL](https://www.postgresql.org/download/) +### Admin -## Usage +```ts title="src/admin/lib/config.ts" +import Medusa from "@medusajs/js-sdk" -In your Medusa application's directory, you can use the Medusa CLI tool using NPX. +export const sdk = new Medusa({ + baseUrl: import.meta.env.VITE_BACKEND_URL || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, +}) +``` -For example: +### Storefront -```bash -npx medusa --help +```ts title="config.ts" +import Medusa from "@medusajs/js-sdk" + +let MEDUSA_BACKEND_URL = "http://localhost:9000" + +if (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) { + MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL +} + +export const sdk = new Medusa({ + baseUrl: MEDUSA_BACKEND_URL, + debug: process.env.NODE_ENV === "development", + publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY, +}) ``` +In the Medusa Admin you use `import.meta.env` to access environment variables becaues the Medusa Admin is built on top of [Vite](https://vitejs.dev/). Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/environment-variables/index.html.md). + +### JS SDK Configurations + +The `Medusa` initializer accepts as a parameter an object with the following properties: + +|Property|Description|Default| +|---|---|---|---|---| +|\`baseUrl\`|A required string indicating the URL to the Medusa backend.|-| +|\`publishableKey\`|A string indicating the publishable API key to use in the storefront. You can retrieve it from the Medusa Admin.|-| +|\`auth.type\`|A string that specifies the user authentication method to use.|-| +|\`auth.jwtTokenStorageKey\`|A string that, when |\`medusa\_auth\_token\`| +|\`auth.jwtTokenStorageMethod\`|A string that, when |\`local\`| +|\`auth.storage\`|This option is only available after Medusa v2.5.1. It's an object or class that's used when |-| +|\`auth.fetchCredentials\`|By default, if |\`local\`| +|\`globalHeaders\`|An object of key-value pairs indicating headers to pass in all requests, where the key indicates the name of the header field.|-| +|\`apiKey\`|A string indicating the admin user's API key. If specified, it's used to send authenticated requests.|-| +|\`debug\`|A boolean indicating whether to show debug messages of requests sent in the console. This is useful during development.|\`false\`| +|\`logger\`|Replace the logger used by the JS SDK to log messages. The logger must be a class or object having the following methods:|JavaScript's | + *** +## Send Requests to Custom Routes -# build Command - Medusa CLI Reference +The sidebar shows the different methods that you can use to send requests to Medusa's API routes. -Create a standalone build of the Medusa application. +To send requests to custom routes, the JS SDK has a `client.fetch` method that wraps the [JavaScript Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) that you can use. The method automatically appends configurations and headers, such as authentication headers, to your request. -This creates a build that: +For example, to send a request to a custom route at `http://localhost:9000/custom`: -- Doesn't rely on the source TypeScript files. -- Can be copied to a production server reliably. +### GET -The build is outputted to a new `.medusa/server` directory. +```ts +sdk.client.fetch(`/custom`) +.then((data) => { + console.log(data) +}) +``` -```bash -npx medusa build +### POST + +```ts +sdk.client.fetch(`/custom`, { + method: "post", + body: { + id: "123", + }, +}).then((data) => { + console.log(data) +}) ``` -Refer to [this section](#run-built-medusa-application) for next steps. - -## Options +### DELETE -|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 | +```ts +sdk.client.fetch(`/custom`, { + method: "delete", +}).then(() => { + console.log("success") +}) +``` -*** +The `fetch` method accepts as a first parameter the route's path relative to the `baseUrl` configuration you passed when you initialized the SDK. -## Run Built Medusa Application +In the second parameter, you can pass an object of [request configurations](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit). You don't need to configure the content-type to be JSON, or stringify the `body` or `query` value, as that's handled by the method. -After running the `build` command, use the following step to run the built Medusa application: +The method returns a Promise that, when resolved, has the data returned by the request. If the request returns a JSON object, it'll be automatically parsed to a JavaScript object and returned. -- Change to the `.medusa/server` directory and install the dependencies: +*** -```bash npm2yarn -cd .medusa/server && npm install -``` +## Medusa JS SDK Tips -- 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. +### Use Tanstack (React) Query in Admin Customizations -```bash npm2yarn -cp .env .medusa/server/.env.production -``` +In admin customizations, use [Tanstack Query](https://tanstack.com/query/latest) with the JS SDK to send requests to custom or existing API routes. -- In the system environment variables, set `NODE_ENV` to `production`: +Tanstack Query is installed by default in your Medusa application. -```bash -NODE_ENV=production -``` +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. -- Use the `start` command to run the application: +Use the [configured SDK](#setup-js-sdk) with the [useQuery](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery#usequery) Tanstack Query hook to send `GET` requests, and [useMutation](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation) hook to send `POST` or `DELETE` requests. -```bash npm2yarn -cd .medusa/server && npm run start -``` +For example: -*** +### Query -## Build Medusa Admin +```tsx title="src/admin/widgets/product-widget.ts" +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" -By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory. +const ProductWidget = () => { + const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list(), + queryKey: ["products"], + }) + + return ( + <Container className="divide-y p-0"> + {isLoading && <span>Loading...</span>} + {data?.products && ( + <ul> + {data.products.map((product) => ( + <li key={product.id}>{product.title}</li> + ))} + </ul> + )} + </Container> + ) +} -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. +export const config = defineWidgetConfig({ + zone: "product.list.before", +}) +export default ProductWidget +``` -# db Commands - Medusa CLI Reference +### Mutation -Commands starting with `db:` perform actions on the database. +```tsx title="src/admin/widgets/product-widget.ts" +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" -## db:setup +const ProductWidget = ({ + data: productData, +}: DetailWidgetProps<HttpTypes.AdminProduct>) => { + const { mutateAsync } = useMutation({ + mutationFn: (payload: HttpTypes.AdminUpdateProduct) => + sdk.admin.product.update(productData.id, payload), + onSuccess: () => alert("updated product"), + }) -Creates a database for the Medusa application with the specified name, if it doesn't exit. Then, it runs migrations and syncs links. + const handleUpdate = () => { + mutateAsync({ + title: "New Product Title", + }) + } + + return ( + <Container className="divide-y p-0"> + <Button onClick={handleUpdate}>Update Title</Button> + </Container> + ) +} -It also updates your `.env` file with the database name. +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) -```bash -npx medusa db:setup --db <name> +export default ProductWidget ``` -Use this command if you're setting up a Medusa project or database manually. +Refer to Tanstack Query's documentation to learn more about sending [Queries](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery#usequery) and [Mutations](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation). -### Options +### Cache in Next.js Projects -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`--db \<name>\`|The database name.|Yes|-| -|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.| -|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| -|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| -|\`--no-interactive\`|Disable the command's prompts.|No|-| +Every method of the SDK that sends requests accepts as a last parameter an object of key-value headers to pass in the request. -*** +In Next.js storefronts or projects, pass the `next.tags` header in the last parameter for data caching. -## db:create +For example: -Creates a database for the Medusa application with the specified name, if it doesn't exit. +```ts highlights={[["2", "next"], ["3", "tags", "An array of tags to cache the data under."]]} +sdk.store.product.list({}, { + next: { + tags: ["products"], + }, +}) +``` -It also updates your `.env` file with the database name. +The `tags` property accepts an array of tags that the data is cached under. -```bash -npx medusa db:create --db <name> -``` +Then, to purge the cache later, use Next.js's `revalidateTag` utility: -Use this command if you want to only create a database. +```ts +import { revalidateTag } from "next/cache" -### Options +// ... -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`--db \<name>\`|The database name.|Yes|-| -|\`--no-interactive\`|Disable the command's prompts.|No|-| +revalidateTag("products") +``` -*** +Learn more in the [Next.js documentation](https://nextjs.org/docs/app/building-your-application/caching#fetch-optionsnexttags-and-revalidatetag). -## db:generate +### Use Custom Storage -Generate a migration file for the latest changes in one or more modules. +The `auth.storage` configuration is only available after Medusa v2.5.1. -```bash -npx medusa db:generate <module_names...> -``` +If you're using the JS SDK in an environment where Local Storage or Session Storage isn't available, such as in a React Native application, you can define custom logic to store the JWT token. -### Arguments +To do that, set the `auth.jwtTokenStorageMethod` configuration to `custom` and define the `auth.storage` configuration with the custom logic to store the JWT token. -|Argument|Description|Required| -|---|---|---|---|---| -|\`module\_names\`|The name of one or more module (separated by spaces) to generate migrations for. For example, |Yes| +For example, if you're using React Native's `AsyncStorage`: -*** +```ts title="config.ts" +import AsyncStorage from "@react-native-async-storage/async-storage" +import Medusa from "@medusajs/js-sdk" -## db:migrate +let MEDUSA_BACKEND_URL = "http://localhost:9000" -Run the latest migrations to reflect changes on the database, sync link definitions with the database, and run migration data scripts. +if (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) { + MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL +} -```bash -npx medusa db:migrate +export const sdk = new Medusa({ + baseUrl: MEDUSA_BACKEND_URL, + debug: process.env.NODE_ENV === "development", + publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY, + auth: { + type: "jwt", + jwtTokenStorageMethod: "custom", + storge: AsyncStorage, + }, +}) ``` -Use this command if you've updated the Medusa packages, or you've created customizations and want to reflect them in the database. +In the `auth` configuration, you specify the `type` as `jwt`, the `jwtTokenStorageMethod` as `custom`, and the `storage` as `AsyncStorage`. So, the SDK uses `AsyncStorage` to store the JWT token. -### Options +#### Custom Storage Methods -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.| -|\`--skip-scripts\`|Skip running data migration scripts. This option is added starting from -|No|Data migration scripts are run by default starting from -| -|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| -|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| +The object or class passed to `auth.storage` configuration must have the following methods: -*** +- `setItem`: A function that accepts a key and value to store the JWT token. +- `getItem`: A function that accepts a key to retrieve the JWT token. +- `removeItem`: A function that accepts a key to remove the JWT token from storage. -## db:rollback -Revert the last migrations ran on one or more modules. +## JS SDK Admin -```bash -npx medusa db:rollback <module_names...> -``` +- [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) +- [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) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/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) +- [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/Campaign/methods/js_sdk.admin.Campaign.retrieve/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) +- [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/index.html.md) +- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md) +- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md) +- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancel/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md) +- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/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) +- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md) +- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/index.html.md) +- [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/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) +- [clearToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken/index.html.md) +- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md) +- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md) +- [clearToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken_/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) +- [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) +- [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/index.html.md) +- [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/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) +- [throwError\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.throwError_/index.html.md) +- [setToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken_/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) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.delete/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) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.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) +- [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) +- [update](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.update/index.html.md) +- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/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) +- [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) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancel/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md) +- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md) +- [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md) +- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/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) +- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md) +- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md) +- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/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) +- [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) +- [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) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md) +- [updateServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.updateServiceZone/index.html.md) +- [batchInventoryItemLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemLocationLevels/index.html.md) +- [batchInventoryItemsLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemsLocationLevels/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) +- [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) +- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md) +- [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/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) +- [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/Notification/methods/js_sdk.admin.Notification.list/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/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/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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/index.html.md) +- [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancel/index.html.md) +- [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md) +- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md) +- [createFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createFulfillment/index.html.md) +- [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.list/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) +- [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md) +- [requestTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.requestTransfer/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.update/index.html.md) +- [retrievePreview](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrievePreview/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrieve/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) +- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/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) +- [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) +- [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) +- [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) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md) +- [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/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/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.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) +- [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) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.create/index.html.md) +- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md) +- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md) +- [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/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) +- [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/index.html.md) +- [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md) +- [listVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listVariants/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md) +- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md) +- [updateOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateOption/index.html.md) +- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/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/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.list/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) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.create/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/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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.retrieve/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/ProductCategory/methods/js_sdk.admin.ProductCategory.create/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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/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) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/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) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.create/index.html.md) +- [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/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) +- [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/index.html.md) +- [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/index.html.md) +- [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/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) +- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.updateRules/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.retrieve/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/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) +- [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/Reservation/methods/js_sdk.admin.Reservation.create/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) +- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/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) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/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) +- [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) +- [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md) +- [removeDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeDismissItem/index.html.md) +- [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md) +- [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md) +- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.retrieve/index.html.md) +- [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/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/ShippingOption/methods/js_sdk.admin.ShippingOption.create/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) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md) +- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/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) +- [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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.list/index.html.md) +- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.updateProducts/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/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) +- [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/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md) +- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md) +- [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md) +- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.create/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/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) +- [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/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) +- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/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/TaxRegion/methods/js_sdk.admin.TaxRegion.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) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/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) +- [me](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.me/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.list/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) -### Arguments -|Argument|Description|Required| -|---|---|---|---|---| -|\`module\_names\`|The name of one or more module (separated by spaces) to rollback their migrations for. For example, |Yes| +## JS SDK Auth -*** +- [callback](https://docs.medusajs.com/references/js-sdk/auth/callback/index.html.md) +- [register](https://docs.medusajs.com/references/js-sdk/auth/register/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) +- [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md) +- [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md) +- [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/index.html.md) -## db:sync-links -Sync the database with the link definitions in your application, including the definitions in Medusa's modules. +## JS SDK Store -```bash -npx medusa db:sync-links -``` +- [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) +- [order](https://docs.medusajs.com/references/js-sdk/store/order/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) +- [region](https://docs.medusajs.com/references/js-sdk/store/region/index.html.md) +- [product](https://docs.medusajs.com/references/js-sdk/store/product/index.html.md) -### Options -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`--execute-safe\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.| -|\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| +# Configure Medusa Backend +In this document, you’ll learn how to create a file service in the Medusa application and the methods you must implement in it. -# exec Command - Medusa CLI Reference +The configurations for your Medusa application are set in `medusa-config.ts` located in the root of your Medusa project. The configurations include configurations for database, modules, and more. -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). +:::note -```bash -npx medusa exec [file] [args...] -``` +Some Medusa configurations are set through environment variables, which you can find in [this documentation](https://docs.medusajs.com/learn/fundamentals/environment-variables#predefined-medusa-environment-variables/index.html.md). -## 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| +`medusa-config.ts` exports the value returned by the `defineConfig` utility function imported from `@medusajs/framework/utils`. +`defineConfig` accepts as a parameter an object with the following properties: -# develop Command - Medusa CLI Reference +- [projectConfig](https://docs.medusajs.com/references/medusa-config#projectconfig/index.html.md) (required): An object that holds general configurations related to the Medusa application, such as database or CORS configurations. +- [plugins](https://docs.medusajs.com/references/medusa-config#plugins/index.html.md): An array of strings or objects that hold the configurations of the plugins installed in the Medusa application. +- [admin](https://docs.medusajs.com/references/medusa-config#admin/index.html.md): An object that holds admin-related configurations. +- [modules](https://docs.medusajs.com/references/medusa-config#modules/index.html.md): An object that configures the Medusa application's modules. +- [featureFlags](https://docs.medusajs.com/references/medusa-config#featureflags/index.html.md): An object that enables or disables features guarded by a feature flag. -Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. +For example: -```bash -npx medusa develop +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + // ... + }, + admin: { + // ... + }, + modules: { + // ... + }, + featureFlags: { + // ... + } +}) ``` -## Options +*** -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| +## Environment Variables +It's highly recommended to store the values of configurations in environment variables, then reference them within `medusa-config.ts`. -# new Command - Medusa CLI Reference +During development, you can set your environment variables in the `.env` file at the root of your Medusa application project. In production, +setting the environment variables depends on the hosting provider. -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 [<dir_name> [<starter_url>]] -``` +## projectConfig -## Arguments +This property holds essential configurations related to the Medusa application, such as database and CORS configurations. -|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\`| +### databaseName -## Options +The name of the database to connect to. If the name is specified in `databaseUrl`, then you don't have to use this configuration. -|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 \<user>\`|The database user to use for database setup.| -|\`--db-database \<database>\`|The name of the database used for database setup.| -|\`--db-pass \<password>\`|The database password to use for database setup.| -|\`--db-port \<port>\`|The database port to use for database setup.| -|\`--db-host \<host>\`|The database host to use for database setup.| +Make sure to create the PostgreSQL database before using it. You can check how to create a database in +[PostgreSQL's documentation](https://www.postgresql.org/docs/current/sql-createdatabase.html). +#### Example -# start-cluster Command - Medusa CLI Reference +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + databaseName: process.env.DATABASE_NAME || + "medusa-store", + // ... + }, + // ... +}) +``` -Starts the Medusa application in [cluster mode](https://expressjs.com/en/advanced/best-practice-performance.html#run-your-app-in-a-cluster). +### databaseUrl -Running in cluster mode significantly improves performance as the workload and tasks are distributed among all available instances instead of a single one. +The PostgreSQL connection URL of the database, which is of the following format: ```bash -npx medusa start-cluster +postgres://[user][:password]@[host][:port]/[dbname] ``` -#### Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-c \<number>\`|The number of CPUs that Medusa can consume.|Medusa will try to consume all CPUs.| -|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| - - -# plugin Commands - Medusa CLI Reference +Where: -Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development. +- `[user]`: (required) your PostgreSQL username. If not specified, the system's username is used by default. The database user that you use must have create privileges. If you're using the `postgres` superuser, then it should have these privileges by default. Otherwise, make sure to grant your user create privileges. You can learn how to do that in [PostgreSQL's documentation](https://www.postgresql.org/docs/current/ddl-priv.html). +- `[:password]`: an optional password for the user. When provided, make sure to put `:` before the password. +- `[host]`: (required) your PostgreSQL host. When run locally, it should be `localhost`. +- `[:port]`: an optional port that the PostgreSQL server is listening on. By default, it's `5432`. When provided, make sure to put `:` before the port. +- `[dbname]`: (required) the name of the database. -These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). +You can learn more about the connection URL format in [PostgreSQL’s documentation](https://www.postgresql.org/docs/current/libpq-connect.html). -## plugin:publish +#### Example -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. +For example, set the following database URL in your environment variables: ```bash -npx medusa plugin:publish +DATABASE_URL=postgres://postgres@localhost/medusa-store ``` -*** - -## 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. +Then, use the value in `medusa-config.ts`: -```bash -npx medusa plugin:add [names...] +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + databaseUrl: process.env.DATABASE_URL, + // ... + }, + // ... +}) ``` -### Arguments +### databaseSchema -|Argument|Description|Required| -|---|---|---|---|---| -|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes| +The database schema to connect to. This is not required to provide if you’re using the default schema, which is `public`. -*** +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + databaseSchema: process.env.DATABASE_SCHEMA || + "custom", + // ... + }, + // ... +}) +``` -## plugin:develop +### databaseLogging -Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry. +This configuration specifies whether database messages should be logged. -```bash -npx medusa plugin:develop +#### Example + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + databaseLogging: false + // ... + }, + // ... +}) ``` -*** +### databaseDriverOptions -## plugin:db:generate +This configuration is used to pass additional options to the database connection. You can pass any configuration. For example, pass the +`ssl` property that enables support for TLS/SSL connections. -Generate migrations for all modules in a plugin. +This is useful for production databases, which can be supported by setting the `rejectUnauthorized` attribute of `ssl` object to `false`. +During development, it’s recommended not to pass this option. -```bash -npx medusa plugin:db:generate -``` +:::note -*** +Make sure to add to the end of the database URL `?ssl_mode=disable` as well when disabling `rejectUnauthorized`. -## plugin:build +::: -Build a plugin before publishing it to NPM. The command will compile an output in the `.medusa/server` directory. +#### Example -```bash -npx medusa plugin:build +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + databaseDriverOptions: process.env.NODE_ENV !== "development" ? + { connection: { ssl: { rejectUnauthorized: false } } } : {} + // ... + }, + // ... +}) ``` +#### Properties -# telemetry Command - Medusa CLI Reference +- connection: (\`object\`) -Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. + - ssl: (\`boolean\` \\| \`ConnectionOptions\`) Configure support for TLS/SSL connection -```bash -npx medusa telemetry -``` +### redisUrl -#### Options +This configuration specifies the connection URL to Redis to store the Medusa server's session. -|Option|Description| -|---|---|---| -|\`--enable\`|Enable telemetry (default).| -|\`--disable\`|Disable telemetry.| +:::note +You must first have Redis installed. You can refer to [Redis's installation guide](https://redis.io/docs/getting-started/installation/). -# user Command - Medusa CLI Reference +::: -Create a new admin user. +The Redis connection URL has the following format: ```bash -npx medusa user --email <email> [--password <password>] +redis[s]://[[username][:password]@][host][:port][/db-number] ``` -## Options +For a local Redis installation, the connection URL should be `redis://localhost:6379` unless you’ve made any changes to the Redis configuration during installation. -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`-e \<email>\`|The user's email.|Yes|-| -|\`-p \<password>\`|The user's password.|No|-| -|\`-i \<id>\`|The user's ID.|No|An automatically generated ID.| -|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password. -If ran successfully, you'll receive the invite token in the output.|No|\`false\`| +#### Example +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + redisUrl: process.env.REDIS_URL || + "redis://localhost:6379", + // ... + }, + // ... +}) +``` -# start Command - Medusa CLI Reference +### redisPrefix -Start the Medusa application in production. +This configuration defines a prefix on all keys stored in Redis for the Medusa server's session. The default value is `sess:`. -```bash -npx medusa start +If this configuration option is provided, it is prepended to `sess:`. + +#### Example + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + redisPrefix: process.env.REDIS_URL || "medusa:", + // ... + }, + // ... +}) ``` -## Options +### redisOptions -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| +This configuration defines options to pass ioredis for the Redis connection used to store the Medusa server's session. Refer to [ioredis’s RedisOptions documentation](https://redis.github.io/ioredis/index.html#RedisOptions) +for the list of available options. +#### Example -# Medusa JS SDK +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + redisOptions: { + connectionName: process.env.REDIS_CONNECTION_NAME || + "medusa", + } + // ... + }, + // ... +}) +``` -In this documentation, you'll learn how to install and use Medusa's JS SDK. +### sessionOptions -## What is Medusa JS SDK? +This configuration defines additional options to pass to [express-session](https://www.npmjs.com/package/express-session), which is used to store the Medusa server's session. -Medusa's JS SDK is a library to easily send requests to your Medusa application. You can use it in your admin customizations or custom storefronts. +#### Example -*** +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + sessionOptions: { + name: process.env.SESSION_NAME || "custom", + } + // ... + }, + // ... +}) +``` -## How to Install Medusa JS SDK? +#### Properties -The Medusa JS SDK is available in your Medusa application by default. So, you don't need to install it before using it in your admin customizations. +- name: (\`string\`) The name of the session ID cookie to set in the response (and read from in the request). The default value is \`connect.sid\`. + Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#name) for more details. +- resave: (\`boolean\`) Whether the session should be saved back to the session store, even if the session was never modified during the request. The default value is \`true\`. + Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#resave) for more details. +- rolling: (\`boolean\`) Whether the session identifier cookie should be force-set on every response. The default value is \`false\`. + Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#rolling) for more details. +- saveUninitialized: (\`boolean\`) Whether a session that is "uninitialized" is forced to be saved to the store. The default value is \`true\`. + Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#saveUninitialized) for more details. +- secret: (\`string\`) The secret to sign the session ID cookie. By default, the value of \`http.cookieSecret\` is used. + Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#secret) for details. +- ttl: (\`number\`) Used when calculating the \`Expires\` \`Set-Cookie\` attribute of cookies. By default, its value is \`10 \* 60 \* 60 \* 1000\`. + Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#cookiemaxage) for details. -To install the Medusa JS SDK in other projects, such as a custom storefront, run the following command: +### workerMode -```bash npm2yarn -npm install @medusajs/js-sdk@latest @medusajs/types@latest -``` +Configure the application's worker mode. -You install two libraries: +Workers are processes running separately from the main application. They're useful for executing long-running or resource-heavy tasks in the background, such as importing products. -- `@medusajs/js-sdk`: the Medusa JS SDK. -- `@medusajs/types`: Medusa's types library, which is useful if you're using TypeScript in your development. +With a worker, these tasks are offloaded to a separate process. So, they won't affect the performance of the main application. -*** + -## Setup JS SDK +Medusa has three runtime modes: -In your project, create the following `config.ts` file: +- Use `shared` to run the application in a single process. +- Use `worker` to run the a worker process only. +- Use `server` to run the application server only. -For admin customizations, create this file at `src/admin/lib/config.ts`. +In production, it's recommended to deploy two instances: -### Admin +1. One having the `workerMode` configuration set to `server`. +2. Another having the `workerMode` configuration set to `worker`. -```ts title="src/admin/lib/config.ts" -import Medusa from "@medusajs/js-sdk" +#### Example -export const sdk = new Medusa({ - baseUrl: import.meta.env.VITE_BACKEND_URL || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + workerMode: process.env.WORKER_MODE || "shared" + // ... }, + // ... }) ``` -### Storefront - -```ts title="config.ts" -import Medusa from "@medusajs/js-sdk" +### http -let MEDUSA_BACKEND_URL = "http://localhost:9000" +This property configures the application's http-specific settings. -if (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) { - MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL -} +#### Example -export const sdk = new Medusa({ - baseUrl: MEDUSA_BACKEND_URL, - debug: process.env.NODE_ENV === "development", - publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY, +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + http: { + cookieSecret: "supersecret", + compression: { + // ... + } + } + // ... + }, + // ... }) ``` -In the Medusa Admin you use `import.meta.env` to access environment variables becaues the Medusa Admin is built on top of [Vite](https://vitejs.dev/). Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/environment-variables/index.html.md). - -### JS SDK Configurations - -The `Medusa` initializer accepts as a parameter an object with the following properties: - -|Property|Description|Default| -|---|---|---|---|---| -|\`baseUrl\`|A required string indicating the URL to the Medusa backend.|-| -|\`publishableKey\`|A string indicating the publishable API key to use in the storefront. You can retrieve it from the Medusa Admin.|-| -|\`auth.type\`|A string that specifies the user authentication method to use.|-| -|\`auth.jwtTokenStorageKey\`|A string that, when |\`medusa\_auth\_token\`| -|\`auth.jwtTokenStorageMethod\`|A string that, when |\`local\`| -|\`auth.storage\`|This option is only available after Medusa v2.5.1. It's an object or class that's used when |-| -|\`auth.fetchCredentials\`|By default, if |\`local\`| -|\`globalHeaders\`|An object of key-value pairs indicating headers to pass in all requests, where the key indicates the name of the header field.|-| -|\`apiKey\`|A string indicating the admin user's API key. If specified, it's used to send authenticated requests.|-| -|\`debug\`|A boolean indicating whether to show debug messages of requests sent in the console. This is useful during development.|\`false\`| -|\`logger\`|Replace the logger used by the JS SDK to log messages. The logger must be a class or object having the following methods:|JavaScript's | - -*** +#### Properties -## Send Requests to Custom Routes +- authCors: (\`string\`) The Medusa application's API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. -The sidebar shows the different methods that you can use to send requests to Medusa's API routes. + \`cors\` is a string used to specify the accepted URLs or patterns for API Routes starting with \`/auth\`. It can either be one accepted origin, or a comma-separated list of accepted origins. -To send requests to custom routes, the JS SDK has a `client.fetch` method that wraps the [JavaScript Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) that you can use. The method automatically appends configurations and headers, such as authentication headers, to your request. + Every origin in that list must either be: -For example, to send a request to a custom route at `http://localhost:9000/custom`: + 1\. A URL. For example, \`http://localhost:7001\`. The URL must not end with a backslash; + 2\. Or a regular expression pattern that can match more than one origin. For example, \`.example.com\`. The regex pattern that Medusa tests for is \`^(\[/~@;%#'])(.\*?)\1(\[gimsuy]\*)$\`. +- storeCors: (\`string\`) The Medusa application's API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. -### GET + \`store\_cors\` is a string used to specify the accepted URLs or patterns for store API Routes. It can either be one accepted origin, or a comma-separated list of accepted origins. -```ts -sdk.client.fetch(`/custom`) -.then((data) => { - console.log(data) -}) -``` + Every origin in that list must either be: -### POST + 1\. A URL. For example, \`http://localhost:8000\`. The URL must not end with a backslash; + 2\. Or a regular expression pattern that can match more than one origin. For example, \`.example.com\`. The regex pattern that the backend tests for is \`^(\[/~@;%#'])(.\*?)\1(\[gimsuy]\*)$\`. +- adminCors: (\`string\`) The Medusa application's API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. -```ts -sdk.client.fetch(`/custom`, { - method: "post", - body: { - id: "123", - }, -}).then((data) => { - console.log(data) -}) -``` + \`admin\_cors\` is a string used to specify the accepted URLs or patterns for admin API Routes. It can either be one accepted origin, or a comma-separated list of accepted origins. -### DELETE + Every origin in that list must either be: -```ts -sdk.client.fetch(`/custom`, { - method: "delete", -}).then(() => { - console.log("success") -}) -``` + 1\. A URL. For example, \`http://localhost:7001\`. The URL must not end with a backslash; + 2\. Or a regular expression pattern that can match more than one origin. For example, \`.example.com\`. The regex pattern that the backend tests for is \`^(\[/~@;%#'])(.\*?)\1(\[gimsuy]\*)$\`. +- jwtSecret: (\`string\`) A random string used to create authentication tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. -The `fetch` method accepts as a first parameter the route's path relative to the `baseUrl` configuration you passed when you initialized the SDK. + In a development environment, if this option is not set the default secret is \`supersecret\`. However, in production, if this configuration is not set, an + error is thrown and the application crashes. +- jwtExpiresIn: (\`string\`) The expiration time for the JWT token. Its format is based off the \[ms package]\(https://github.com/vercel/ms). -In the second parameter, you can pass an object of [request configurations](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit). You don't need to configure the content-type to be JSON, or stringify the `body` or `query` value, as that's handled by the method. + If not provided, the default value is \`24h\`. +- cookieSecret: (\`string\`) A random string used to create cookie tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. -The method returns a Promise that, when resolved, has the data returned by the request. If the request returns a JSON object, it'll be automatically parsed to a JavaScript object and returned. + In a development environment, if this option is not set, the default secret is \`supersecret\`. However, in production, if this configuration is not set, an error is thrown and + the application crashes. +- compression: (\[HttpCompressionOptions]\(../medusa\_config.HttpCompressionOptions/page.mdx)) Configure HTTP compression from the application layer. If you have access to the HTTP server, the recommended approach would be to enable it there. + However, some platforms don't offer access to the HTTP layer and in those cases, this is a good alternative. -*** + If you enable HTTP compression and you want to disable it for specific API Routes, you can pass in the request header \`"x-no-compression": true\`. + Learn more in the \[API Reference]\(https://docs.medusajs.com/api/store#http-compression). -## Medusa JS SDK Tips + - enabled: (\`boolean\`) Whether HTTP compression is enabled. By default, it's \`false\`. -### Use Tanstack (React) Query in Admin Customizations + - level: (\`number\`) The level of zlib compression to apply to responses. A higher level will result in better compression but will take longer to complete. + A lower level will result in less compression but will be much faster. The default value is \`6\`. -In admin customizations, use [Tanstack Query](https://tanstack.com/query/latest) with the JS SDK to send requests to custom or existing API routes. + - memLevel: (\`number\`) How much memory should be allocated to the internal compression state. It's an integer in the range of 1 (minimum level) and 9 (maximum level). + The default value is \`8\`. -Tanstack Query is installed by default in your Medusa application. + - threshold: (\`string\` \\| \`number\`) The minimum response body size that compression is applied on. Its value can be the number of bytes or any string accepted by the + \[bytes]\(https://www.npmjs.com/package/bytes) module. The default value is \`1024\`. +- authMethodsPerActor: (\`Record\<string, string\[]>\`) This configuration specifies the supported authentication providers per actor type (such as \`user\`, \`customer\`, or any custom actors). + For example, you only want to allow SSO logins for \`users\`, while you want to allow email/password logins for \`customers\` to the storefront. -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. + \`authMethodsPerActor\` is a a map where the actor type (eg. 'user') is the key, and the value is an array of supported auth provider IDs. +- restrictedFields: (\`object\`) Specifies the fields that can't be selected in the response unless specified in the allowed query config. + This is useful to restrict sensitive fields from being exposed in the API. -Use the [configured SDK](#setup-js-sdk) with the [useQuery](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery#usequery) Tanstack Query hook to send `GET` requests, and [useMutation](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation) hook to send `POST` or `DELETE` requests. + - store: (\`string\`\[]) -For example: +*** -### Query +## admin -```tsx title="src/admin/widgets/product-widget.ts" -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" +This property holds configurations for the Medusa Admin dashboard. -const ProductWidget = () => { - const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list(), - queryKey: ["products"], - }) - - return ( - <Container className="divide-y p-0"> - {isLoading && <span>Loading...</span>} - {data?.products && ( - <ul> - {data.products.map((product) => ( - <li key={product.id}>{product.title}</li> - ))} - </ul> - )} - </Container> - ) -} +### Example -export const config = defineWidgetConfig({ - zone: "product.list.before", +```ts title="medusa-config.ts" +module.exports = defineConfig({ + admin: { + backendUrl: process.env.MEDUSA_BACKEND_URL || + "http://localhost:9000" + }, + // ... }) - -export default ProductWidget ``` -### Mutation - -```tsx title="src/admin/widgets/product-widget.ts" -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" +### disable -const ProductWidget = ({ - data: productData, -}: DetailWidgetProps<HttpTypes.AdminProduct>) => { - const { mutateAsync } = useMutation({ - mutationFn: (payload: HttpTypes.AdminUpdateProduct) => - sdk.admin.product.update(productData.id, payload), - onSuccess: () => alert("updated product"), - }) +Whether to disable the admin dashboard. If set to `true`, the admin dashboard is disabled, +in both development and production environments. The default value is `false`. - const handleUpdate = () => { - mutateAsync({ - title: "New Product Title", - }) - } - - return ( - <Container className="divide-y p-0"> - <Button onClick={handleUpdate}>Update Title</Button> - </Container> - ) -} +#### Example -export const config = defineWidgetConfig({ - zone: "product.details.before", +```ts title="medusa-config.ts" +module.exports = defineConfig({ + admin: { + disable: process.env.ADMIN_DISABLED === "true" || + false + }, + // ... }) - -export default ProductWidget ``` -Refer to Tanstack Query's documentation to learn more about sending [Queries](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery#usequery) and [Mutations](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation). +### path -### Cache in Next.js Projects +The path to the admin dashboard. The default value is `/app`. -Every method of the SDK that sends requests accepts as a last parameter an object of key-value headers to pass in the request. +The value cannot be one of the reserved paths: -In Next.js storefronts or projects, pass the `next.tags` header in the last parameter for data caching. +- `/admin` +- `/store` +- `/auth` +- `/` -For example: +:::note -```ts highlights={[["2", "next"], ["3", "tags", "An array of tags to cache the data under."]]} -sdk.store.product.list({}, { - next: { - tags: ["products"], +When using Docker, make sure that the root path of the Docker image doesn't path the admin's `path`. For example, if the Docker image's root path is `/app`, change +the value of the `path` configuration, as it's `/app` by default. + +::: + +#### Example + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + admin: { + path: process.env.ADMIN_PATH || `/app`, }, + // ... }) ``` -The `tags` property accepts an array of tags that the data is cached under. - -Then, to purge the cache later, use Next.js's `revalidateTag` utility: +### outDir -```ts -import { revalidateTag } from "next/cache" +The directory where the admin build is outputted when you run the `build` command. +The default value is `./build`. -// ... +#### Example -revalidateTag("products") +```ts title="medusa-config.ts" +module.exports = defineConfig({ + admin: { + outDir: process.env.ADMIN_BUILD_DIR || `./build`, + }, + // ... +}) ``` -Learn more in the [Next.js documentation](https://nextjs.org/docs/app/building-your-application/caching#fetch-optionsnexttags-and-revalidatetag). - -### Use Custom Storage - -The `auth.storage` configuration is only available after Medusa v2.5.1. +### backendUrl -If you're using the JS SDK in an environment where Local Storage or Session Storage isn't available, such as in a React Native application, you can define custom logic to store the JWT token. +The URL of your Medusa application. Defaults to the browser origin. This is useful to set when running the admin on a separate domain. -To do that, set the `auth.jwtTokenStorageMethod` configuration to `custom` and define the `auth.storage` configuration with the custom logic to store the JWT token. +#### Example -For example, if you're using React Native's `AsyncStorage`: +```ts title="medusa-config.ts" +module.exports = defineConfig({ + admin: { + backendUrl: process.env.MEDUSA_BACKEND_URL || + "http://localhost:9000" + }, + // ... +}) +``` -```ts title="config.ts" -import AsyncStorage from "@react-native-async-storage/async-storage" -import Medusa from "@medusajs/js-sdk" +### storefrontUrl -let MEDUSA_BACKEND_URL = "http://localhost:9000" +The URL of your Medusa storefront application. This URL is used as a prefix to some +links in the admin that require performing actions in the storefront. For example, +this URL is used as a prefix to shareable payment links for orders with +outstanding amounts. -if (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) { - MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL -} +#### Example -export const sdk = new Medusa({ - baseUrl: MEDUSA_BACKEND_URL, - debug: process.env.NODE_ENV === "development", - publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY, - auth: { - type: "jwt", - jwtTokenStorageMethod: "custom", - storge: AsyncStorage, +```js title="medusa-config.js" +module.exports = defineConfig({ + admin: { + storefrontUrl: process.env.MEDUSA_STOREFRONT_URL || + "http://localhost:8000" }, + // ... }) ``` -In the `auth` configuration, you specify the `type` as `jwt`, the `jwtTokenStorageMethod` as `custom`, and the `storage` as `AsyncStorage`. So, the SDK uses `AsyncStorage` to store the JWT token. - -#### Custom Storage Methods +### vite -The object or class passed to `auth.storage` configuration must have the following methods: +Configure the Vite configuration for the admin dashboard. This function receives the default Vite configuration +and returns the modified configuration. The default value is `undefined`. -- `setItem`: A function that accepts a key and value to store the JWT token. -- `getItem`: A function that accepts a key to retrieve the JWT token. -- `removeItem`: A function that accepts a key to remove the JWT token from storage. +Learn about configurations you can pass to Vite in [Vite's documentation](https://vite.dev/config/). +#### Example -## JS SDK Admin +For example, if you're using a third-party library that isn't ESM-compatible, add it to Vite's `optimizeDeps` configuration: -- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/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) -- [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) -- [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) -- [revoke](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.revoke/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) -- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md) -- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md) -- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md) -- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md) -- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/index.html.md) -- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/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) -- [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md) -- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md) -- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md) -- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md) -- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundShipping/index.html.md) -- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundItem/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) -- [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md) -- [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/index.html.md) -- [getToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken_/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) -- [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) -- [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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.list/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) -- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/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/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) -- [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/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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.create/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.update/index.html.md) -- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/index.html.md) -- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md) -- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/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) -- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/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) -- [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) -- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/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) -- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md) -- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md) -- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md) -- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.delete/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) -- [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/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md) -- [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/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/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) -- [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/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) -- [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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.delete/index.html.md) -- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.list/index.html.md) -- [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md) -- [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancel/index.html.md) -- [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md) -- [createFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createFulfillment/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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.list/index.html.md) -- [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md) -- [listChanges](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listChanges/index.html.md) -- [markAsDelivered](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.markAsDelivered/index.html.md) -- [requestTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.requestTransfer/index.html.md) -- [retrievePreview](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrievePreview/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.update/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) -- [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md) -- [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/index.html.md) -- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md) -- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md) -- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md) -- [updateOriginalItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateOriginalItem/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md) -- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md) -- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/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/Payment/methods/js_sdk.admin.Payment.retrieve/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) -- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/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/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) -- [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) -- [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/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md) -- [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/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) -- [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/index.html.md) -- [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.create/index.html.md) -- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md) -- [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/index.html.md) -- [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md) -- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md) -- [export](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.export/index.html.md) -- [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/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) -- [listVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listVariants/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md) -- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md) -- [updateOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateOption/index.html.md) -- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.retrieve/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) -- [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) -- [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) -- [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/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) -- [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) -- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/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) -- [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) -- [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/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.create/index.html.md) -- [addRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.addRules/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) -- [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/index.html.md) -- [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/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) -- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.updateRules/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/Region/methods/js_sdk.admin.Region.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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.create/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.list/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) -- [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md) -- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md) -- [confirmReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmReceive/index.html.md) -- [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md) -- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelRequest/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) -- [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md) -- [dismissItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.dismissItems/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) -- [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md) -- [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.retrieve/index.html.md) -- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md) -- [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md) -- [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/index.html.md) -- [updateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateRequest/index.html.md) -- [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md) -- [updateReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnItem/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.create/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) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/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) -- [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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.list/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) -- [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) -- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md) -- [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md) -- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/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/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) -- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/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/Upload/methods/js_sdk.admin.Upload.delete/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/Upload/methods/js_sdk.admin.Upload.create/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) -- [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/User/methods/js_sdk.admin.User.list/index.html.md) -- [me](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.me/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) +```ts title="medusa-config.ts" +module.exports = defineConfig({ + admin: { + vite: () => { + return { + optimizeDeps: { + include: ["qs"], + }, + }; + }, + }, + // ... +}) +``` +*** -## JS SDK Auth +## plugins -- [callback](https://docs.medusajs.com/references/js-sdk/auth/callback/index.html.md) -- [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/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) -- [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) +On your Medusa server, you can use [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) to add re-usable Medusa customizations. Plugins +can include modules, workflows, API Routes, and other customizations. Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). +Aside from installing the plugin with NPM, you need to pass the plugin you installed into the `plugins` array defined in `medusa-config.ts`. -## JS SDK Store +The items in the array can either be: -- [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) -- [customer](https://docs.medusajs.com/references/js-sdk/store/customer/index.html.md) -- [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/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) +- A string, which is the name of the plugin's package as specified in the plugin's `package.json` file. You can pass a plugin as a string if it doesn’t require any options. +- An object having the following properties: + - `resolve`: The name of the plugin's package as specified in the plugin's `package.json` file. + - `options`: An object that includes options to be passed to the modules within the plugin. Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). +Learn how to create a plugin in [this documentation](https://docs.medusajs.com/learn/fundamentals/plugins/create/index.html.md). -# Configure Medusa Backend +### Example -In this document, you’ll learn how to create a file service in the Medusa application and the methods you must implement in it. +```ts title="medusa-config.ts" +module.exports = { + plugins: [ + `medusa-my-plugin-1`, + { + resolve: `medusa-my-plugin`, + options: { + apiKey: process.env.MY_API_KEY || + `test`, + }, + }, + // ... + ], + // ... +} +``` -The configurations for your Medusa application are set in `medusa-config.ts` located in the root of your Medusa project. The configurations include configurations for database, modules, and more. +### resolve + +The name of the plugin's package as specified in the plugin's `package.json` file. + +### options + +An object that includes options to be passed to the modules within the plugin. +Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). + +*** + +## modules + +This property holds all custom modules installed in your Medusa application. :::note -Some Medusa configurations are set through environment variables, which you can find in [this documentation](https://docs.medusajs.com/learn/fundamentals/environment-variables#predefined-medusa-environment-variables/index.html.md). +Medusa's commerce modules are configured by default, so only +add them to this property if you're changing their configurations or adding providers to a module. ::: -`medusa-config.ts` exports the value returned by the `defineConfig` utility function imported from `@medusajs/framework/utils`. +`modules` is an array of objects, each holding a module's registration configurations. Each object has the following properties: -`defineConfig` accepts as a parameter an object with the following properties: +1. `resolve`: a string indicating the path to the module relative to `src`, or the module's NPM package name. For example, `./modules/my-module`. +2. `options`: (optional) an object indicating the options to pass to the module. -- [projectConfig](https://docs.medusajs.com/references/medusa-config#projectconfig/index.html.md) (required): An object that holds general configurations related to the Medusa application, such as database or CORS configurations. -- [plugins](https://docs.medusajs.com/references/medusa-config#plugins/index.html.md): An array of strings or objects that hold the configurations of the plugins installed in the Medusa application. -- [admin](https://docs.medusajs.com/references/medusa-config#admin/index.html.md): An object that holds admin-related configurations. -- [modules](https://docs.medusajs.com/references/medusa-config#modules/index.html.md): An object that configures the Medusa application's modules. -- [featureFlags](https://docs.medusajs.com/references/medusa-config#featureflags/index.html.md): An object that enables or disables features guarded by a feature flag. +### Example -For example: +```ts title="medusa-config.ts" +module.exports = defineConfig({ + modules: [ + { + resolve: "./modules/hello" + } + ] + // ... +}) +``` + +*** + +## featureFlags + +Some features in the Medusa application are guarded by a feature flag. This ensures constant shipping of new features while maintaining the engine’s stability. + +You can enable a feature in your application by enabling its feature flag. Feature flags are enabled through either environment +variables or through this configuration property exported in `medusa-config.ts`. + +The `featureFlags`'s value is an object. Its properties are the names of the feature flags, and their value is a boolean indicating whether the feature flag is enabled. + +You can find available feature flags and their key name [here](https://github.com/medusajs/medusa/tree/develop/packages/medusa/src/loaders/feature-flags). + +### Example ```ts title="medusa-config.ts" module.exports = defineConfig({ - projectConfig: { - // ... - }, - admin: { - // ... - }, - modules: { - // ... - }, featureFlags: { + analytics: true, // ... } + // ... }) ``` +:::note + +After enabling a feature flag, make sure to run migrations as it may require making changes to the database. + +::: + + +# Admin Components + +In this section, you'll find examples of implementing common Medusa Admin components and layouts. + +These components are useful to follow the same design conventions as the Medusa Admin, and are build on top of the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md). + +Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.md) for a full list of components. + +## Layouts + +Use these components to set the layout of your UI route. + *** -## Environment Variables +## Components -It's highly recommended to store the values of configurations in environment variables, then reference them within `medusa-config.ts`. +Use these components in your widgets and UI routes. -During development, you can set your environment variables in the `.env` file at the root of your Medusa application project. In production, -setting the environment variables depends on the hosting provider. + +# Single Column Layout - Admin Components + +The Medusa Admin has pages with a single column of content. + +This doesn't include the sidebar, only the main content. + + + +To create a layout that you can use in UI routes to support one column of content, create the component `src/admin/layouts/single-column.tsx` with the following content: + +```tsx title="src/admin/layouts/single-column.tsx" +export type SingleColumnLayoutProps = { + children: React.ReactNode +} + +export const SingleColumnLayout = ({ children }: SingleColumnLayoutProps) => { + return ( + <div className="flex flex-col gap-y-3"> + {children} + </div> + ) +} +``` + +The `SingleColumnLayout` accepts the content in the `children` props. *** -## projectConfig +## Example -This property holds essential configurations related to the Medusa application, such as database and CORS configurations. +Use the `SingleColumnLayout` component in your UI routes that have a single column. For example: -### databaseName +```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 { SingleColumnLayout } from "../../layouts/single-column" +import { Header } from "../../components/header" -The name of the database to connect to. If the name is specified in `databaseUrl`, then you don't have to use this configuration. +const CustomPage = () => { + return ( + <SingleColumnLayout> + <Container> + <Header title="Custom Page" /> + </Container> + </SingleColumnLayout> + ) +} -Make sure to create the PostgreSQL database before using it. You can check how to create a database in -[PostgreSQL's documentation](https://www.postgresql.org/docs/current/sql-createdatabase.html). +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) -#### Example +export default CustomPage +``` -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - databaseName: process.env.DATABASE_NAME || - "medusa-store", - // ... - }, - // ... +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. + + +# 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. + + + +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 ( + <div className="flex flex-col gap-x-4 gap-y-3 xl:flex-row xl:items-start"> + <div className="flex w-full flex-col gap-y-3"> + {firstCol} + </div> + <div className="flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[440px]"> + {secondCol} + </div> + </div> + ) +} +``` + +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 ( + <TwoColumnLayout + firstCol={ + <Container> + <Header title="First Column" /> + </Container> + } + secondCol={ + <Container> + <Header title="Second Column" /> + </Container> + } + /> + ) +} + +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, }) + +export default CustomPage ``` -### databaseUrl +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. -The PostgreSQL connection URL of the database, which is of the following format: -```bash -postgres://[user][:password]@[host][:port]/[dbname] +# Action Menu - Admin Components + +The Medusa Admin often provides additional actions in a dropdown shown when users click a three-dot icon. + + + +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/components/action-menu.tsx" +import { + DropdownMenu, + IconButton, + clx, +} 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[] +} + +export type ActionMenuProps = { + groups: ActionGroup[] +} + +export const ActionMenu = ({ groups }: ActionMenuProps) => { + return ( + <DropdownMenu> + <DropdownMenu.Trigger asChild> + <IconButton size="small" variant="transparent"> + <EllipsisHorizontal /> + </IconButton> + </DropdownMenu.Trigger> + <DropdownMenu.Content> + {groups.map((group, index) => { + if (!group.actions.length) { + return null + } + + const isLast = index === groups.length - 1 + + return ( + <DropdownMenu.Group key={index}> + {group.actions.map((action, index) => { + if (action.onClick) { + return ( + <DropdownMenu.Item + disabled={action.disabled} + key={index} + onClick={(e) => { + 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} + <span>{action.label}</span> + </DropdownMenu.Item> + ) + } + + return ( + <div key={index}> + <DropdownMenu.Item + className={clx( + "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2", + { + "[&_svg]:text-ui-fg-disabled": action.disabled, + } + )} + asChild + disabled={action.disabled} + > + <Link to={action.to} onClick={(e) => e.stopPropagation()}> + {action.icon} + <span>{action.label}</span> + </Link> + </DropdownMenu.Item> + </div> + ) + })} + {!isLast && <DropdownMenu.Separator />} + </DropdownMenu.Group> + ) + })} + </DropdownMenu.Content> + </DropdownMenu> + ) +} ``` -Where: +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. -- `[user]`: (required) your PostgreSQL username. If not specified, the system's username is used by default. The database user that you use must have create privileges. If you're using the `postgres` superuser, then it should have these privileges by default. Otherwise, make sure to grant your user create privileges. You can learn how to do that in [PostgreSQL's documentation](https://www.postgresql.org/docs/current/ddl-priv.html). -- `[:password]`: an optional password for the user. When provided, make sure to put `:` before the password. -- `[host]`: (required) your PostgreSQL host. When run locally, it should be `localhost`. -- `[:port]`: an optional port that the PostgreSQL server is listening on. By default, it's `5432`. When provided, make sure to put `:` before the port. -- `[dbname]`: (required) the name of the database. +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 ( + <Container> + <ActionMenu groups={[ + { + actions: [ + { + icon: <Pencil />, + label: "Edit", + onClick: () => { + alert("You clicked the edit action!") + }, + }, + ], + }, + ]} /> + </Container> + ) +} + +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 ( + <Container> + <Header + title="Product Widget" + subtitle="This is my custom product widget" + actions={[ + { + type: "action-menu", + props: { + groups: [ + { + actions: [ + { + icon: <Pencil />, + label: "Edit", + onClick: () => { + alert("You clicked the edit action!") + }, + }, + ], + }, + ], + }, + }, + ]} + /> + </Container> + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` -You can learn more about the connection URL format in [PostgreSQL’s documentation](https://www.postgresql.org/docs/current/libpq-connect.html). -#### Example +# Data Table - Admin Components -For example, set the following database URL in your environment variables: +This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). -```bash -DATABASE_URL=postgres://postgres@localhost/medusa-store -``` +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. -Then, use the value in `medusa-config.ts`: + -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - databaseUrl: process.env.DATABASE_URL, - // ... - }, - // ... -}) -``` +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. -### databaseSchema +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. -The database schema to connect to. This is not required to provide if you’re using the default schema, which is `public`. +## Example: DataTable with Data Fetching -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - databaseSchema: process.env.DATABASE_SCHEMA || - "custom", - // ... - }, - // ... -}) -``` +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. -### databaseLogging +Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI: -This configuration specifies whether database messages should be logged. +```tsx title="src/admin/routes/custom/page.tsx" +import { + createDataTableColumnHelper, +} from "@medusajs/ui" +import { + HttpTypes, +} from "@medusajs/framework/types" -#### Example +const columnHelper = createDataTableColumnHelper<HttpTypes.AdminProduct>() -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - databaseLogging: false - // ... - }, - // ... -}) +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 ( + <Badge color={status === "published" ? "green" : "grey"} size="xsmall"> + {status === "published" ? "Published" : "Draft"} + </Badge> + ) + }, + }), +] ``` -### databaseDriverOptions - -This configuration is used to pass additional options to the database connection. You can pass any configuration. For example, pass the -`ssl` property that enables support for TLS/SSL connections. +`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: -This is useful for production databases, which can be supported by setting the `rejectUnauthorized` attribute of `ssl` object to `false`. -During development, it’s recommended not to pass this option. +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". -:::note +Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status. -Make sure to add to the end of the database URL `?ssl_mode=disable` as well when disabling `rejectUnauthorized`. +To define the filters, add the following: -::: +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { + // ... + createDataTableFilterHelper, +} from "@medusajs/ui" -#### Example +const filterHelper = createDataTableFilterHelper<HttpTypes.AdminProduct>() -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - databaseDriverOptions: process.env.NODE_ENV !== "development" ? - { connection: { ssl: { rejectUnauthorized: false } } } : {} - // ... - }, - // ... -}) +const filters = [ + filterHelper.accessor("status", { + type: "select", + label: "Status", + options: [ + { + label: "Published", + value: "published", + }, + { + label: "Draft", + value: "draft", + }, + ], + }), +] ``` -#### Properties - -- connection: (\`object\`) +`createDataTableFilterHelper` utility creates a filter helper that helps you define the filters for the data table. The filter helper has an `accessor` method that accepts two parameters: - - ssl: (\`boolean\` \\| \`ConnectionOptions\`) Configure support for TLS/SSL connection +1. The key of a column in the table's data. +2. An object with the following properties: + - `type`: The type of filter. It can be either: + - `select`: A select dropdown allowing users to choose multiple values. + - `radio`: A radio button allowing users to choose one value. + - `date`: A date picker allowing users to choose a date. + - `label`: The filter's label. + - `options`: An array of objects with `label` and `value` properties. The `label` is the option's label, and the `value` is the value to filter by. -### redisUrl +You'll now start creating the UI widget's component. Start by adding the necessary state variables: -This configuration specifies the connection URL to Redis to store the Medusa server's session. +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { + // ... + DataTablePaginationState, + DataTableFilteringState, + DataTableSortingState, +} from "@medusajs/ui" +import { useMemo, useState } from "react" -:::note +// ... -You must first have Redis installed. You can refer to [Redis's installation guide](https://redis.io/docs/getting-started/installation/). +const limit = 15 -::: +const CustomPage = () => { + const [pagination, setPagination] = useState<DataTablePaginationState>({ + pageSize: limit, + pageIndex: 0, + }) + const [search, setSearch] = useState<string>("") + const [filtering, setFiltering] = useState<DataTableFilteringState>({}) + const [sorting, setSorting] = useState<DataTableSortingState | null>(null) -The Redis connection URL has the following format: + const offset = useMemo(() => { + return pagination.pageIndex * limit + }, [pagination]) + const statusFilters = useMemo(() => { + return (filtering.status || []) as ProductStatus + }, [filtering]) -```bash -redis[s]://[[username][:password]@][host][:port][/db-number] + // TODO add data fetching logic +} ``` -For a local Redis installation, the connection URL should be `redis://localhost:6379` unless you’ve made any changes to the Redis configuration during installation. +In the component, you've added the following state variables: -#### Example +- `pagination`: An object of type `DataTablePaginationState` that holds the pagination state. It has two properties: + - `pageSize`: The number of items to show per page. + - `pageIndex`: The current page index. +- `search`: A string that holds the search query. +- `filtering`: An object of type `DataTableFilteringState` that holds the filtering state. +- `sorting`: An object of type `DataTableSortingState` that holds the sorting state. -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - redisUrl: process.env.REDIS_URL || - "redis://localhost:6379", - // ... - }, - // ... -}) -``` +You've also added two memoized variables: -### redisPrefix +- `offset`: How many items to skip when fetching data based on the current page. +- `statusFilters`: The selected status filters, if any. -This configuration defines a prefix on all keys stored in Redis for the Medusa server's session. The default value is `sess:`. +Next, you'll fetch the products from the Medusa application. Assuming you have the JS SDK configured as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), add the following imports at the top of the file: -If this configuration option is provided, it is prepended to `sess:`. +```tsx title="src/admin/routes/custom/page.tsx" +import { sdk } from "../../lib/config" +import { useQuery } from "@tanstack/react-query" +``` -#### Example +This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest). -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - redisPrefix: process.env.REDIS_URL || "medusa:", - // ... - }, - // ... +Then, replace the `TODO` in the component with the following: + +```tsx title="src/admin/routes/custom/page.tsx" +const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list({ + limit, + offset, + q: search, + status: statusFilters, + order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, + }), + queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], }) + +// TODO configure data table ``` -### redisOptions +You use the `useQuery` hook to fetch the products from the Medusa application. In the `queryFn`, you call the `sdk.admin.product.list` method to fetch the products. You pass the following query parameters to the method: -This configuration defines options to pass ioredis for the Redis connection used to store the Medusa server's session. Refer to [ioredis’s RedisOptions documentation](https://redis.github.io/ioredis/index.html#RedisOptions) -for the list of available options. +- `limit`: The number of products to fetch per page. +- `offset`: The number of products to skip based on the current page. +- `q`: The search query, if set. +- `status`: The status filters, if set. +- `order`: The sorting order, if set. -#### Example +So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters. -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - redisOptions: { - connectionName: process.env.REDIS_CONNECTION_NAME || - "medusa", - } - // ... - }, +Next, you'll configure the data table. Medusa UI provides a `useDataTable` hook that helps you configure the data table. Add the following imports at the top of the file: + +```tsx title="src/admin/routes/custom/page.tsx" +import { // ... -}) + useDataTable, +} from "@medusajs/ui" +import { useNavigate } from "react-router-dom" ``` -### sessionOptions - -This configuration defines additional options to pass to [express-session](https://www.npmjs.com/package/express-session), which is used to store the Medusa server's session. +Then, replace the `TODO` in the component with the following: -#### Example +```tsx title="src/admin/routes/custom/page.tsx" +const navigate = useNavigate() -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - sessionOptions: { - name: process.env.SESSION_NAME || "custom", - } - // ... +const table = useDataTable({ + columns, + data: data?.products || [], + getRowId: (row) => row.id, + rowCount: data?.count || 0, + isLoading, + pagination: { + state: pagination, + onPaginationChange: setPagination, + }, + search: { + state: search, + onSearchChange: setSearch, + }, + filtering: { + state: filtering, + onFilteringChange: setFiltering, + }, + filters, + sorting: { + // Pass the pagination state and updater to the table instance + state: sorting, + onSortingChange: setSorting, + }, + onRowClick: (event, row) => { + // Handle row click, for example + navigate(`/products/${row.id}`) }, - // ... }) -``` - -#### Properties - -- name: (\`string\`) The name of the session ID cookie to set in the response (and read from in the request). The default value is \`connect.sid\`. - Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#name) for more details. -- resave: (\`boolean\`) Whether the session should be saved back to the session store, even if the session was never modified during the request. The default value is \`true\`. - Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#resave) for more details. -- rolling: (\`boolean\`) Whether the session identifier cookie should be force-set on every response. The default value is \`false\`. - Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#rolling) for more details. -- saveUninitialized: (\`boolean\`) Whether a session that is "uninitialized" is forced to be saved to the store. The default value is \`true\`. - Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#saveUninitialized) for more details. -- secret: (\`string\`) The secret to sign the session ID cookie. By default, the value of \`http.cookieSecret\` is used. - Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#secret) for details. -- ttl: (\`number\`) Used when calculating the \`Expires\` \`Set-Cookie\` attribute of cookies. By default, its value is \`10 \* 60 \* 60 \* 1000\`. - Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#cookiemaxage) for details. -### workerMode +// TODO render component +``` -Configure the application's worker mode. +The `useDataTable` hook accepts an object with the following properties: -Workers are processes running separately from the main application. They're useful for executing long-running or resource-heavy tasks in the background, such as importing products. +- columns: (\`array\`) The columns to display in the data table. You created this using the \`createDataTableColumnHelper\` utility. +- data: (\`array\`) The products fetched from the Medusa application. +- getRowId: (\`function\`) A function that returns the unique ID of a row. +- rowCount: (\`number\`) The total number of products that can be retrieved. This is used to determine the number of pages. +- isLoading: (\`boolean\`) A boolean that indicates if the data is being fetched. +- pagination: (\`object\`) An object to configure pagination. -With a worker, these tasks are offloaded to a separate process. So, they won't affect the performance of the main application. + - state: (\`object\`) The pagination React state variable. - + - onPaginationChange: (\`function\`) A function that updates the pagination state. +- search: (\`object\`) An object to configure searching. -Medusa has three runtime modes: + - state: (\`string\`) The search query React state variable. -- Use `shared` to run the application in a single process. -- Use `worker` to run the a worker process only. -- Use `server` to run the application server only. + - onSearchChange: (\`function\`) A function that updates the search query state. +- filtering: (\`object\`) An object to configure filtering. -In production, it's recommended to deploy two instances: + - state: (\`object\`) The filtering React state variable. -1. One having the `workerMode` configuration set to `server`. -2. Another having the `workerMode` configuration set to `worker`. + - onFilteringChange: (\`function\`) A function that updates the filtering state. +- filters: (\`array\`) The filters to display in the data table. You created this using the \`createDataTableFilterHelper\` utility. +- sorting: (\`object\`) An object to configure sorting. -#### Example + - state: (\`object\`) The sorting React state variable. -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - workerMode: process.env.WORKER_MODE || "shared" - // ... - }, - // ... -}) -``` + - onSortingChange: (\`function\`) A function that updates the sorting state. +- onRowClick: (\`function\`) A function that allows you to perform an action when the user clicks on a row. In this example, you navigate to the product's detail page. -### http + - event: (\`mouseevent\`) An instance of the \[MouseClickEvent]\(https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object. -This property configures the application's http-specific settings. + - row: (\`object\`) The data of the row that was clicked. -#### Example +Finally, you'll render the data table. But first, add the following imports at the top of the page: -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - http: { - cookieSecret: "supersecret", - compression: { - // ... - } - } - // ... - }, +```tsx title="src/admin/routes/custom/page.tsx" +import { // ... -}) + DataTable, +} from "@medusajs/ui" +import { SingleColumnLayout } from "../../layouts/single-column" +import { Container } from "../../components/container" ``` -#### Properties - -- authCors: (\`string\`) The Medusa application's API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. - - \`cors\` is a string used to specify the accepted URLs or patterns for API Routes starting with \`/auth\`. It can either be one accepted origin, or a comma-separated list of accepted origins. - - Every origin in that list must either be: - - 1\. A URL. For example, \`http://localhost:7001\`. The URL must not end with a backslash; - 2\. Or a regular expression pattern that can match more than one origin. For example, \`.example.com\`. The regex pattern that Medusa tests for is \`^(\[/~@;%#'])(.\*?)\1(\[gimsuy]\*)$\`. -- storeCors: (\`string\`) The Medusa application's API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. - - \`store\_cors\` is a string used to specify the accepted URLs or patterns for store API Routes. It can either be one accepted origin, or a comma-separated list of accepted origins. - - Every origin in that list must either be: - - 1\. A URL. For example, \`http://localhost:8000\`. The URL must not end with a backslash; - 2\. Or a regular expression pattern that can match more than one origin. For example, \`.example.com\`. The regex pattern that the backend tests for is \`^(\[/~@;%#'])(.\*?)\1(\[gimsuy]\*)$\`. -- adminCors: (\`string\`) The Medusa application's API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. +Aside from the `DataTable` component, you also import the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md) and [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard. - \`admin\_cors\` is a string used to specify the accepted URLs or patterns for admin API Routes. It can either be one accepted origin, or a comma-separated list of accepted origins. +Then, replace the `TODO` in the component with the following: - Every origin in that list must either be: +```tsx title="src/admin/routes/custom/page.tsx" +return ( + <SingleColumnLayout> + <Container> + <DataTable instance={table}> + <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center"> + <Heading>Products</Heading> + <div className="flex gap-2"> + <DataTable.FilterMenu tooltip="Filter" /> + <DataTable.SortingMenu tooltip="Sort" /> + <DataTable.Search placeholder="Search..." /> + </div> + </DataTable.Toolbar> + <DataTable.Table /> + <DataTable.Pagination /> + </DataTable> + </Container> + </SingleColumnLayout> +) +``` - 1\. A URL. For example, \`http://localhost:7001\`. The URL must not end with a backslash; - 2\. Or a regular expression pattern that can match more than one origin. For example, \`.example.com\`. The regex pattern that the backend tests for is \`^(\[/~@;%#'])(.\*?)\1(\[gimsuy]\*)$\`. -- jwtSecret: (\`string\`) A random string used to create authentication tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. +You render the `DataTable` component and pass the `table` instance as a prop. In the `DataTable` component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table. - In a development environment, if this option is not set the default secret is \`supersecret\`. However, in production, if this configuration is not set, an - error is thrown and the application crashes. -- jwtExpiresIn: (\`string\`) The expiration time for the JWT token. Its format is based off the \[ms package]\(https://github.com/vercel/ms). +Lastly, export the component and the UI widget's configuration at the end of the file: - If not provided, the default value is \`24h\`. -- cookieSecret: (\`string\`) A random string used to create cookie tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" - In a development environment, if this option is not set, the default secret is \`supersecret\`. However, in production, if this configuration is not set, an error is thrown and - the application crashes. -- compression: (\[HttpCompressionOptions]\(../medusa\_config.HttpCompressionOptions/page.mdx)) Configure HTTP compression from the application layer. If you have access to the HTTP server, the recommended approach would be to enable it there. - However, some platforms don't offer access to the HTTP layer and in those cases, this is a good alternative. +// ... - If you enable HTTP compression and you want to disable it for specific API Routes, you can pass in the request header \`"x-no-compression": true\`. - Learn more in the \[API Reference]\(https://docs.medusajs.com/api/store#http-compression). +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) - - enabled: (\`boolean\`) Whether HTTP compression is enabled. By default, it's \`false\`. +export default CustomPage +``` - - level: (\`number\`) The level of zlib compression to apply to responses. A higher level will result in better compression but will take longer to complete. - A lower level will result in less compression but will be much faster. The default value is \`6\`. +If you start your Medusa application and go to `localhost:9000/app/custom`, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities. - - memLevel: (\`number\`) How much memory should be allocated to the internal compression state. It's an integer in the range of 1 (minimum level) and 9 (maximum level). - The default value is \`8\`. +### Full Example Code - - threshold: (\`string\` \\| \`number\`) The minimum response body size that compression is applied on. Its value can be the number of bytes or any string accepted by the - \[bytes]\(https://www.npmjs.com/package/bytes) module. The default value is \`1024\`. -- authMethodsPerActor: (\`Record\<string, string\[]>\`) This configuration specifies the supported authentication providers per actor type (such as \`user\`, \`customer\`, or any custom actors). - For example, you only want to allow SSO logins for \`users\`, while you want to allow email/password logins for \`customers\` to the storefront. +```tsx title="src/admin/routes/custom/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" +import { + Badge, + createDataTableColumnHelper, + createDataTableFilterHelper, + DataTable, + DataTableFilteringState, + DataTablePaginationState, + DataTableSortingState, + Heading, + useDataTable, +} from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { SingleColumnLayout } from "../../layouts/single-column" +import { sdk } from "../../lib/config" +import { useMemo, useState } from "react" +import { Container } from "../../components/container" +import { HttpTypes, ProductStatus } from "@medusajs/framework/types" - \`authMethodsPerActor\` is a a map where the actor type (eg. 'user') is the key, and the value is an array of supported auth provider IDs. -- restrictedFields: (\`object\`) Specifies the fields that can't be selected in the response unless specified in the allowed query config. - This is useful to restrict sensitive fields from being exposed in the API. +const columnHelper = createDataTableColumnHelper<HttpTypes.AdminProduct>() - - store: (\`string\`\[]) +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 ( + <Badge color={status === "published" ? "green" : "grey"} size="xsmall"> + {status === "published" ? "Published" : "Draft"} + </Badge> + ) + }, + }), +] -*** +const filterHelper = createDataTableFilterHelper<HttpTypes.AdminProduct>() -## admin +const filters = [ + filterHelper.accessor("status", { + type: "select", + label: "Status", + options: [ + { + label: "Published", + value: "published", + }, + { + label: "Draft", + value: "draft", + }, + ], + }), +] -This property holds configurations for the Medusa Admin dashboard. +const limit = 15 -### Example +const CustomPage = () => { + const [pagination, setPagination] = useState<DataTablePaginationState>({ + pageSize: limit, + pageIndex: 0, + }) + const [search, setSearch] = useState<string>("") + const [filtering, setFiltering] = useState<DataTableFilteringState>({}) + const [sorting, setSorting] = useState<DataTableSortingState | null>(null) -```ts title="medusa-config.ts" -module.exports = defineConfig({ - admin: { - backendUrl: process.env.MEDUSA_BACKEND_URL || - "http://localhost:9000" - }, - // ... -}) -``` + const offset = useMemo(() => { + return pagination.pageIndex * limit + }, [pagination]) + const statusFilters = useMemo(() => { + return (filtering.status || []) as ProductStatus + }, [filtering]) -### disable + const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list({ + limit, + offset, + q: search, + status: statusFilters, + order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, + }), + queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], + }) -Whether to disable the admin dashboard. If set to `true`, the admin dashboard is disabled, -in both development and production environments. The default value is `false`. + const table = useDataTable({ + columns, + data: data?.products || [], + getRowId: (row) => row.id, + rowCount: data?.count || 0, + isLoading, + pagination: { + state: pagination, + onPaginationChange: setPagination, + }, + search: { + state: search, + onSearchChange: setSearch, + }, + filtering: { + state: filtering, + onFilteringChange: setFiltering, + }, + filters, + sorting: { + // Pass the pagination state and updater to the table instance + state: sorting, + onSortingChange: setSorting, + }, + }) -#### Example + return ( + <SingleColumnLayout> + <Container> + <DataTable instance={table}> + <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center"> + <Heading>Products</Heading> + <div className="flex gap-2"> + <DataTable.FilterMenu tooltip="Filter" /> + <DataTable.SortingMenu tooltip="Sort" /> + <DataTable.Search placeholder="Search..." /> + </div> + </DataTable.Toolbar> + <DataTable.Table /> + <DataTable.Pagination /> + </DataTable> + </Container> + </SingleColumnLayout> + ) +} -```ts title="medusa-config.ts" -module.exports = defineConfig({ - admin: { - disable: process.env.ADMIN_DISABLED === "true" || - false - }, - // ... +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, }) -``` - -### path -The path to the admin dashboard. The default value is `/app`. +export default CustomPage +``` -The value cannot be one of the reserved paths: -- `/admin` -- `/store` -- `/auth` -- `/` +# Forms - Admin Components -:::note +The Medusa Admin has two types of forms: -When using Docker, make sure that the root path of the Docker image doesn't path the admin's `path`. For example, if the Docker image's root path is `/app`, change -the value of the `path` configuration, as it's `/app` by default. +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. -#### Example +## Form Tooling -```ts title="medusa-config.ts" -module.exports = defineConfig({ - admin: { - path: process.env.ADMIN_PATH || `/app`, - }, - // ... -}) -``` +The Medusa Admin uses the following tools to build the forms: -### outDir +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. -The directory where the admin build is outputted when you run the `build` command. -The default value is `./build`. +Both of these libraries are available in your project, so you don't have to install them to use them. -#### Example +*** -```ts title="medusa-config.ts" -module.exports = defineConfig({ - admin: { - outDir: process.env.ADMIN_BUILD_DIR || `./build`, - }, - // ... -}) -``` +## Create Form -### backendUrl +In this section, you'll build a form component to create an item of a resource. -The URL of your Medusa application. Defaults to the browser origin. This is useful to set when running the admin on a separate domain. +### 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" -```ts title="medusa-config.ts" -module.exports = defineConfig({ - admin: { - backendUrl: process.env.MEDUSA_BACKEND_URL || - "http://localhost:9000" - }, - // ... +const schema = zod.object({ + name: zod.string(), }) -``` - -### storefrontUrl -The URL of your Medusa storefront application. This URL is used as a prefix to some -links in the admin that require performing actions in the storefront. For example, -this URL is used as a prefix to shareable payment links for orders with -outstanding amounts. +export const CreateForm = () => { + const form = useForm<zod.infer<typeof schema>>({ + defaultValues: { + name: "", + }, + }) -#### Example + const handleSubmit = form.handleSubmit(({ name }) => { + // TODO submit to backend + console.log(name) + }) -```js title="medusa-config.js" -module.exports = defineConfig({ - admin: { - storefrontUrl: process.env.MEDUSA_STOREFRONT_URL || - "http://localhost:8000" - }, - // ... -}) + return ( + <FocusModal> + <FocusModal.Trigger asChild> + <Button>Create</Button> + </FocusModal.Trigger> + <FocusModal.Content> + <FormProvider {...form}> + <form + onSubmit={handleSubmit} + className="flex h-full flex-col overflow-hidden" + > + <FocusModal.Header> + <div className="flex items-center justify-end gap-x-2"> + <FocusModal.Close asChild> + <Button size="small" variant="secondary"> + Cancel + </Button> + </FocusModal.Close> + <Button type="submit" size="small"> + Save + </Button> + </div> + </FocusModal.Header> + <FocusModal.Body> + <div className="flex flex-1 flex-col items-center overflow-y-auto"> + <div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16"> + <div> + <Heading className="capitalize"> + Create Item + </Heading> + </div> + <div className="grid grid-cols-2 gap-4"> + <Controller + control={form.control} + name="name" + render={({ field }) => { + return ( + <div className="flex flex-col space-y-2"> + <div className="flex items-center gap-x-1"> + <Label size="small" weight="plus"> + Name + </Label> + </div> + <Input {...field} /> + </div> + ) + }} + /> + </div> + </div> + </div> + </FocusModal.Body> + </form> + </FormProvider> + </FocusModal.Content> + </FocusModal> + ) +} ``` -### vite +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. -Configure the Vite configuration for the admin dashboard. This function receives the default Vite configuration -and returns the modified configuration. The default value is `undefined`. +Start by creating the file `src/admin/components/create-form.tsx` that you'll create the form in. -Learn about configurations you can pass to Vite in [Vite's documentation](https://vite.dev/config/). +### Create Validation Schema -#### Example +In `src/admin/components/create-form.tsx`, create a validation schema with Zod for the form's fields: -For example, if you're using a third-party library that isn't ESM-compatible, add it to Vite's `optimizeDeps` configuration: +```tsx title="src/admin/components/create-form.tsx" +import * as zod from "zod" -```ts title="medusa-config.ts" -module.exports = defineConfig({ - admin: { - vite: () => { - return { - optimizeDeps: { - include: ["qs"], - }, - }; - }, - }, - // ... +const schema = zod.object({ + name: zod.string(), }) ``` -*** +The form in this guide is simple, it only has a required `name` field, which is a string. -## plugins +### Initialize Form -On your Medusa server, you can use [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) to add re-usable Medusa customizations. Plugins -can include modules, workflows, API Routes, and other customizations. Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). +Next, you'll initialize the form using `react-hook-form`. -Aside from installing the plugin with NPM, you need to pass the plugin you installed into the `plugins` array defined in `medusa-config.ts`. +Add to `src/admin/components/create-form.tsx` the following: -The items in the array can either be: +```tsx title="src/admin/components/create-form.tsx" +// other imports... +import { useForm } from "react-hook-form" -- A string, which is the name of the plugin's package as specified in the plugin's `package.json` file. You can pass a plugin as a string if it doesn’t require any options. -- An object having the following properties: - - `resolve`: The name of the plugin's package as specified in the plugin's `package.json` file. - - `options`: An object that includes options to be passed to the modules within the plugin. Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). +// validation schema... -Learn how to create a plugin in [this documentation](https://docs.medusajs.com/learn/fundamentals/plugins/create/index.html.md). +export const CreateForm = () => { + const form = useForm<zod.infer<typeof schema>>({ + defaultValues: { + name: "", + }, + }) -### Example + const handleSubmit = form.handleSubmit(({ name }) => { + // TODO submit to backend + console.log(name) + }) -```ts title="medusa-config.ts" -module.exports = { - plugins: [ - `medusa-my-plugin-1`, - { - resolve: `medusa-my-plugin`, - options: { - apiKey: process.env.MY_API_KEY || - `test`, - }, - }, - // ... - ], - // ... + // TODO render form } ``` -### resolve - -The name of the plugin's package as specified in the plugin's `package.json` file. - -### options - -An object that includes options to be passed to the modules within the plugin. -Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). - -*** - -## modules - -This property holds all custom modules installed in your Medusa application. +You create the `CreateForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form. -:::note +You also define a `handleSubmit` function to perform an action when the form is submitted. -Medusa's commerce modules are configured by default, so only -add them to this property if you're changing their configurations or adding providers to a module. +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. -::: +### Render Components -`modules` is an array of objects, each holding a module's registration configurations. Each object has the following properties: +You'll now add a `return` statement that renders the focus modal where the form is shown. -1. `resolve`: a string indicating the path to the module relative to `src`, or the module's NPM package name. For example, `./modules/my-module`. -2. `options`: (optional) an object indicating the options to pass to the module. +Replace `// TODO render form` with the following: -### Example +```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" -```ts title="medusa-config.ts" -module.exports = defineConfig({ - modules: [ - { - resolve: "./modules/hello" - } - ] +export const CreateForm = () => { // ... -}) -``` - -*** - -## featureFlags -Some features in the Medusa application are guarded by a feature flag. This ensures constant shipping of new features while maintaining the engine’s stability. - -You can enable a feature in your application by enabling its feature flag. Feature flags are enabled through either environment -variables or through this configuration property exported in `medusa-config.ts`. + return ( + <FocusModal> + <FocusModal.Trigger asChild> + <Button>Create</Button> + </FocusModal.Trigger> + <FocusModal.Content> + <FormProvider {...form}> + <form + onSubmit={handleSubmit} + className="flex h-full flex-col overflow-hidden" + > + <FocusModal.Header> + <div className="flex items-center justify-end gap-x-2"> + <FocusModal.Close asChild> + <Button size="small" variant="secondary"> + Cancel + </Button> + </FocusModal.Close> + <Button type="submit" size="small"> + Save + </Button> + </div> + </FocusModal.Header> + <FocusModal.Body> + <div className="flex flex-1 flex-col items-center overflow-y-auto"> + <div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16"> + <div> + <Heading className="capitalize"> + Create Item + </Heading> + </div> + <div className="grid grid-cols-2 gap-4"> + <Controller + control={form.control} + name="name" + render={({ field }) => { + return ( + <div className="flex flex-col space-y-2"> + <div className="flex items-center gap-x-1"> + <Label size="small" weight="plus"> + Name + </Label> + </div> + <Input {...field} /> + </div> + ) + }} + /> + </div> + </div> + </div> + </FocusModal.Body> + </form> + </FormProvider> + </FocusModal.Content> + </FocusModal> + ) +} +``` -The `featureFlags`'s value is an object. Its properties are the names of the feature flags, and their value is a boolean indicating whether the feature flag is enabled. +You render a focus modal, with a trigger button to open it. -You can find available feature flags and their key name [here](https://github.com/medusajs/medusa/tree/develop/packages/medusa/src/loaders/feature-flags). +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. -### Example +In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event. -```ts title="medusa-config.ts" -module.exports = defineConfig({ - featureFlags: { - analytics: true, - // ... - } - // ... -}) -``` +In the `FocusModal.Header` component, you add buttons to save or cancel the form submission. -:::note +Finally, you render the form's components inside the `FocusModal.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`. -After enabling a feature flag, make sure to run migrations as it may require making changes to the database. +### 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: -# Admin Components +```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" -In this section, you'll find examples of implementing common Medusa Admin components and layouts. +const ProductWidget = () => { + return ( + <Container> + <Header + title="Items" + actions={[ + { + type: "custom", + children: <CreateForm />, + }, + ]} + /> + </Container> + ) +} -These components are useful to follow the same design conventions as the Medusa Admin, and are build on top of the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md). +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) -Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.md) for a full list of components. +export default ProductWidget +``` -## Layouts +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. -Use these components to set the layout of your UI route. +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. *** -## Components - -Use these components in your widgets and UI routes. - +## Edit Form -# Single Column Layout - Admin Components +In this section, you'll build a form component to edit an item of a resource. -The Medusa Admin has pages with a single column of content. +### Full Component -This doesn't include the sidebar, only the main content. +```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(), +}) -To create a layout that you can use in UI routes to support one column of content, create the component `src/admin/layouts/single-column.tsx` with the following content: +export const EditForm = () => { + const form = useForm<zod.infer<typeof schema>>({ + defaultValues: { + name: "", + }, + }) -```tsx title="src/admin/layouts/single-column.tsx" -export type SingleColumnLayoutProps = { - children: React.ReactNode -} + const handleSubmit = form.handleSubmit(({ name }) => { + // TODO submit to backend + console.log(name) + }) -export const SingleColumnLayout = ({ children }: SingleColumnLayoutProps) => { return ( - <div className="flex flex-col gap-y-3"> - {children} - </div> + <Drawer> + <Drawer.Trigger asChild> + <Button>Edit Item</Button> + </Drawer.Trigger> + <Drawer.Content> + <FormProvider {...form}> + <form + onSubmit={handleSubmit} + className="flex flex-1 flex-col overflow-hidden" + > + <Drawer.Header> + <Heading className="capitalize"> + Edit Item + </Heading> + </Drawer.Header> + <Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto"> + <Controller + control={form.control} + name="name" + render={({ field }) => { + return ( + <div className="flex flex-col space-y-2"> + <div className="flex items-center gap-x-1"> + <Label size="small" weight="plus"> + Name + </Label> + </div> + <Input {...field} /> + </div> + ) + }} + /> + </Drawer.Body> + <Drawer.Footer> + <div className="flex items-center justify-end gap-x-2"> + <Drawer.Close asChild> + <Button size="small" variant="secondary"> + Cancel + </Button> + </Drawer.Close> + <Button size="small" type="submit"> + Save + </Button> + </div> + </Drawer.Footer> + </form> + </FormProvider> + </Drawer.Content> + </Drawer> ) } ``` -The `SingleColumnLayout` accepts the content in the `children` props. - -*** +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. -## Example +Start by creating the file `src/admin/components/edit-form.tsx` that you'll create the form in. -Use the `SingleColumnLayout` component in your UI routes that have a single column. For example: +### Create Validation Schema -```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 { SingleColumnLayout } from "../../layouts/single-column" -import { Header } from "../../components/header" +In `src/admin/components/edit-form.tsx`, create a validation schema with Zod for the form's fields: -const CustomPage = () => { - return ( - <SingleColumnLayout> - <Container> - <Header title="Custom Page" /> - </Container> - </SingleColumnLayout> - ) -} +```tsx title="src/admin/components/edit-form.tsx" +import * as zod from "zod" -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, +const schema = zod.object({ + name: zod.string(), }) - -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. +The form in this guide is simple, it only has a required `name` field, which is a string. +### Initialize Form -# Two Column Layout - Admin Components +Next, you'll initialize the form using `react-hook-form`. -The Medusa Admin has pages with two columns of content. +Add to `src/admin/components/edit-form.tsx` the following: -This doesn't include the sidebar, only the main content. +```tsx title="src/admin/components/edit-form.tsx" +// other imports... +import { useForm } from "react-hook-form" - +// validation schema... -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: +export const EditForm = () => { + const form = useForm<zod.infer<typeof schema>>({ + defaultValues: { + name: "", + }, + }) -```tsx title="src/admin/layouts/two-column.tsx" -export type TwoColumnLayoutProps = { - firstCol: React.ReactNode - secondCol: React.ReactNode -} + const handleSubmit = form.handleSubmit(({ name }) => { + // TODO submit to backend + console.log(name) + }) -export const TwoColumnLayout = ({ - firstCol, - secondCol, -}: TwoColumnLayoutProps) => { - return ( - <div className="flex flex-col gap-x-4 gap-y-3 xl:flex-row xl:items-start"> - <div className="flex w-full flex-col gap-y-3"> - {firstCol} - </div> - <div className="flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[440px]"> - {secondCol} - </div> - </div> - ) + // TODO render form } ``` -The `TwoColumnLayout` accepts two props: +You create the `EditForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form. -- `firstCol` indicating the content of the first column. -- `secondCol` indicating the content of the second column. +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. -## Example +### Render Components -Use the `TwoColumnLayout` component in your UI routes that have a single column. For example: +You'll now add a `return` statement that renders the drawer where the form is shown. -```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" +Replace `// TODO render form` with the following: + +```tsx title="src/admin/components/edit-form.tsx" +// other imports... +import { + Drawer, + Heading, + Label, + Input, + Button, +} from "@medusajs/ui" +import { + FormProvider, + Controller, +} from "react-hook-form" + +export const EditForm = () => { + // ... -const CustomPage = () => { return ( - <TwoColumnLayout - firstCol={ - <Container> - <Header title="First Column" /> - </Container> - } - secondCol={ - <Container> - <Header title="Second Column" /> - </Container> - } - /> + <Drawer> + <Drawer.Trigger asChild> + <Button>Edit Item</Button> + </Drawer.Trigger> + <Drawer.Content> + <FormProvider {...form}> + <form + onSubmit={handleSubmit} + className="flex flex-1 flex-col overflow-hidden" + > + <Drawer.Header> + <Heading className="capitalize"> + Edit Item + </Heading> + </Drawer.Header> + <Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto"> + <Controller + control={form.control} + name="name" + render={({ field }) => { + return ( + <div className="flex flex-col space-y-2"> + <div className="flex items-center gap-x-1"> + <Label size="small" weight="plus"> + Name + </Label> + </div> + <Input {...field} /> + </div> + ) + }} + /> + </Drawer.Body> + <Drawer.Footer> + <div className="flex items-center justify-end gap-x-2"> + <Drawer.Close asChild> + <Button size="small" variant="secondary"> + Cancel + </Button> + </Drawer.Close> + <Button size="small" type="submit"> + Save + </Button> + </div> + </Drawer.Footer> + </form> + </FormProvider> + </Drawer.Content> + </Drawer> ) } - -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. +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. -# Action Menu - Admin Components +In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event. -The Medusa Admin often provides additional actions in a dropdown shown when users click a three-dot icon. +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. -To create a component that shows this menu in your customizations, create the file `src/admin/components/action-menu.tsx` with the following content: +### Use Edit Form Component -```tsx title="src/admin/components/action-menu.tsx" -import { - DropdownMenu, - IconButton, - clx, -} from "@medusajs/ui" -import { EllipsisHorizontal } from "@medusajs/icons" -import { Link } from "react-router-dom" +You can use the `EditForm` component in your widget or UI route. -export type Action = { - icon: React.ReactNode - label: string - disabled?: boolean -} & ( - | { - to: string - onClick?: never - } - | { - onClick: () => void - to?: never - } -) +For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: -export type ActionGroup = { - actions: Action[] -} +```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 { EditForm } from "../components/edit-form" -export type ActionMenuProps = { - groups: ActionGroup[] +const ProductWidget = () => { + return ( + <Container> + <Header + title="Items" + actions={[ + { + type: "custom", + children: <EditForm />, + }, + ]} + /> + </Container> + ) } -export const ActionMenu = ({ groups }: ActionMenuProps) => { - return ( - <DropdownMenu> - <DropdownMenu.Trigger asChild> - <IconButton size="small" variant="transparent"> - <EllipsisHorizontal /> - </IconButton> - </DropdownMenu.Trigger> - <DropdownMenu.Content> - {groups.map((group, index) => { - if (!group.actions.length) { - return null - } +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) - const isLast = index === groups.length - 1 +export default ProductWidget +``` - return ( - <DropdownMenu.Group key={index}> - {group.actions.map((action, index) => { - if (action.onClick) { - return ( - <DropdownMenu.Item - disabled={action.disabled} - key={index} - onClick={(e) => { - 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} - <span>{action.label}</span> - </DropdownMenu.Item> - ) - } +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. - return ( - <div key={index}> - <DropdownMenu.Item - className={clx( - "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2", - { - "[&_svg]:text-ui-fg-disabled": action.disabled, - } - )} - asChild - disabled={action.disabled} - > - <Link to={action.to} onClick={(e) => e.stopPropagation()}> - {action.icon} - <span>{action.label}</span> - </Link> - </DropdownMenu.Item> - </div> - ) - })} - {!isLast && <DropdownMenu.Separator />} - </DropdownMenu.Group> - ) - })} - </DropdownMenu.Content> - </DropdownMenu> - ) -} -``` +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. -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. +# Header - Admin Components -The component accepts the following props: +Each section in the Medusa Admin has a header with a title, and optionally a subtitle with buttons to perform an action. -- groups: (\`object\[]\`) Groups of actions to be shown in the dropdown. Each group is separated by a divider. + - - actions: (\`object\[]\`) Actions in the group. +To create a component that uses the same header styling and structure, create the file `src/admin/components/header.tsx` with the following content: - - icon: (\`React.ReactNode\`) +```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" - - label: (\`string\`) The action's text. +export type HeadingProps = { + title: string + subtitle?: string + actions?: ( + { + type: "button", + props: React.ComponentProps<typeof Button> + link?: LinkProps + } | + { + type: "action-menu" + props: ActionMenuProps + } | + { + type: "custom" + children: React.ReactNode + } + )[] +} - - disabled: (\`boolean\`) Whether the action is shown as disabled. +export const Header = ({ + title, + subtitle, + actions = [], +}: HeadingProps) => { + return ( + <div className="flex items-center justify-between px-6 py-4"> + <div> + <Heading level="h2">{title}</Heading> + {subtitle && ( + <Text className="text-ui-fg-subtle" size="small"> + {subtitle} + </Text> + )} + </div> + {actions.length > 0 && ( + <div className="flex items-center justify-center gap-x-2"> + {actions.map((action, index) => ( + <> + {action.type === "button" && ( + <Button + {...action.props} + size={action.props.size || "small"} + key={index} + > + <> + {action.props.children} + {action.link && <Link {...action.link} />} + </> + </Button> + )} + {action.type === "action-menu" && ( + <ActionMenu {...action.props} /> + )} + {action.type === "custom" && action.children} + </> + ))} + </div> + )} + </div> + ) +} +``` - - \`to\`: (\`string\`) The link to take the user to when they click the action. This is required if \`onClick\` isn't provided. +The `Header` component shows a title, and optionally a subtitle and action buttons. - - \`onClick\`: (\`() => void\`) The function to execute when the action is clicked. This is required if \`to\` isn't provided. +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. -*** +It accepts the following props: -## Example +- title: (\`string\`) The section's title. +- subtitle: (\`string\`) The section's subtitle. +- actions: (\`object\[]\`) An array of actions to show. -Use the `ActionMenu` component in any widget or UI route. + - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add. -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: + \- If its value is \`button\`, it'll show a button that can have a link or an on-click action. -```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" + \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions. -const ProductWidget = () => { - return ( - <Container> - <ActionMenu groups={[ - { - actions: [ - { - icon: <Pencil />, - label: "Edit", - onClick: () => { - alert("You clicked the edit action!") - }, - }, - ], - }, - ]} /> - </Container> - ) -} + \- If its value is \`custom\`, you can pass any React nodes to render. -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) + - props: (object) -export default ProductWidget -``` + - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions. -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 +## Example -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. +Use the `Header` component in any widget or UI route. -For example: +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 { Header } from "../components/header" @@ -28820,21 +30174,13 @@ const ProductWidget = () => { subtitle="This is my custom product widget" actions={[ { - type: "action-menu", + type: "button", props: { - groups: [ - { - actions: [ - { - icon: <Pencil />, - label: "Edit", - onClick: () => { - alert("You clicked the edit action!") - }, - }, - ], - }, - ], + children: "Click me", + variant: "secondary", + onClick: () => { + alert("You clicked the button.") + }, }, }, ]} @@ -28850,6 +30196,8 @@ export const config = defineWidgetConfig({ 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. + # Container - Admin Components @@ -28881,521 +30229,34 @@ The `Container` component re-uses the component from the [Medusa UI package](htt *** -## 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: - -```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 ( - <Container> - <Header title="Product Widget" /> - </Container> - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -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. - - - -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<HttpTypes.AdminProduct>() - -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 ( - <Badge color={status === "published" ? "green" : "grey"} size="xsmall"> - {status === "published" ? "Published" : "Draft"} - </Badge> - ) - }, - }), -] -``` - -`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<HttpTypes.AdminProduct>() - -const filters = [ - filterHelper.accessor("status", { - type: "select", - label: "Status", - options: [ - { - label: "Published", - value: "published", - }, - { - label: "Draft", - value: "draft", - }, - ], - }), -] -``` - -`createDataTableFilterHelper` utility creates a filter helper that helps you define the filters for the data table. The filter helper has an `accessor` method that accepts two parameters: - -1. The key of a column in the table's data. -2. An object with the following properties: - - `type`: The type of filter. It can be either: - - `select`: A select dropdown allowing users to choose multiple values. - - `radio`: A radio button allowing users to choose one value. - - `date`: A date picker allowing users to choose a date. - - `label`: The filter's label. - - `options`: An array of objects with `label` and `value` properties. The `label` is the option's label, and the `value` is the value to filter by. - -You'll now start creating the UI widget's component. Start by adding the necessary state variables: - -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { - // ... - DataTablePaginationState, - DataTableFilteringState, - DataTableSortingState, -} from "@medusajs/ui" -import { useMemo, useState } from "react" - -// ... - -const limit = 15 - -const CustomPage = () => { - const [pagination, setPagination] = useState<DataTablePaginationState>({ - pageSize: limit, - pageIndex: 0, - }) - const [search, setSearch] = useState<string>("") - const [filtering, setFiltering] = useState<DataTableFilteringState>({}) - const [sorting, setSorting] = useState<DataTableSortingState | null>(null) - - const offset = useMemo(() => { - return pagination.pageIndex * limit - }, [pagination]) - const statusFilters = useMemo(() => { - return (filtering.status || []) as ProductStatus - }, [filtering]) - - // TODO add data fetching logic -} -``` - -In the component, you've added the following state variables: - -- `pagination`: An object of type `DataTablePaginationState` that holds the pagination state. It has two properties: - - `pageSize`: The number of items to show per page. - - `pageIndex`: The current page index. -- `search`: A string that holds the search query. -- `filtering`: An object of type `DataTableFilteringState` that holds the filtering state. -- `sorting`: An object of type `DataTableSortingState` that holds the sorting state. - -You've also added two memoized variables: - -- `offset`: How many items to skip when fetching data based on the current page. -- `statusFilters`: The selected status filters, if any. - -Next, you'll fetch the products from the Medusa application. Assuming you have the JS SDK configured as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), add the following imports at the top of the file: - -```tsx title="src/admin/routes/custom/page.tsx" -import { sdk } from "../../lib/config" -import { useQuery } from "@tanstack/react-query" -``` - -This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest). - -Then, replace the `TODO` in the component with the following: - -```tsx title="src/admin/routes/custom/page.tsx" -const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list({ - limit, - offset, - q: search, - status: statusFilters, - order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, - }), - queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], -}) - -// TODO configure data table -``` - -You use the `useQuery` hook to fetch the products from the Medusa application. In the `queryFn`, you call the `sdk.admin.product.list` method to fetch the products. You pass the following query parameters to the method: - -- `limit`: The number of products to fetch per page. -- `offset`: The number of products to skip based on the current page. -- `q`: The search query, if set. -- `status`: The status filters, if set. -- `order`: The sorting order, if set. - -So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters. - -Next, you'll configure the data table. Medusa UI provides a `useDataTable` hook that helps you configure the data table. Add the following imports at the top of the file: - -```tsx title="src/admin/routes/custom/page.tsx" -import { - // ... - useDataTable, -} from "@medusajs/ui" -import { useNavigate } from "react-router-dom" -``` - -Then, replace the `TODO` in the component with the following: - -```tsx title="src/admin/routes/custom/page.tsx" -const navigate = useNavigate() - -const table = useDataTable({ - columns, - data: data?.products || [], - getRowId: (row) => row.id, - rowCount: data?.count || 0, - isLoading, - pagination: { - state: pagination, - onPaginationChange: setPagination, - }, - search: { - state: search, - onSearchChange: setSearch, - }, - filtering: { - state: filtering, - onFilteringChange: setFiltering, - }, - filters, - sorting: { - // Pass the pagination state and updater to the table instance - state: sorting, - onSortingChange: setSorting, - }, - onRowClick: (event, row) => { - // Handle row click, for example - navigate(`/products/${row.id}`) - }, -}) - -// TODO render component -``` - -The `useDataTable` hook accepts an object with the following properties: - -- columns: (\`array\`) The columns to display in the data table. You created this using the \`createDataTableColumnHelper\` utility. -- data: (\`array\`) The products fetched from the Medusa application. -- getRowId: (\`function\`) A function that returns the unique ID of a row. -- rowCount: (\`number\`) The total number of products that can be retrieved. This is used to determine the number of pages. -- isLoading: (\`boolean\`) A boolean that indicates if the data is being fetched. -- pagination: (\`object\`) An object to configure pagination. - - - state: (\`object\`) The pagination React state variable. - - - onPaginationChange: (\`function\`) A function that updates the pagination state. -- search: (\`object\`) An object to configure searching. - - - state: (\`string\`) The search query React state variable. - - - onSearchChange: (\`function\`) A function that updates the search query state. -- filtering: (\`object\`) An object to configure filtering. - - - state: (\`object\`) The filtering React state variable. - - - onFilteringChange: (\`function\`) A function that updates the filtering state. -- filters: (\`array\`) The filters to display in the data table. You created this using the \`createDataTableFilterHelper\` utility. -- sorting: (\`object\`) An object to configure sorting. - - - state: (\`object\`) The sorting React state variable. - - - onSortingChange: (\`function\`) A function that updates the sorting state. -- onRowClick: (\`function\`) A function that allows you to perform an action when the user clicks on a row. In this example, you navigate to the product's detail page. - - - event: (\`mouseevent\`) An instance of the \[MouseClickEvent]\(https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object. - - - row: (\`object\`) The data of the row that was clicked. - -Finally, you'll render the data table. But first, add the following imports at the top of the page: - -```tsx title="src/admin/routes/custom/page.tsx" -import { - // ... - DataTable, -} from "@medusajs/ui" -import { SingleColumnLayout } from "../../layouts/single-column" -import { Container } from "../../components/container" -``` - -Aside from the `DataTable` component, you also import the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md) and [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard. - -Then, replace the `TODO` in the component with the following: - -```tsx title="src/admin/routes/custom/page.tsx" -return ( - <SingleColumnLayout> - <Container> - <DataTable instance={table}> - <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center"> - <Heading>Products</Heading> - <div className="flex gap-2"> - <DataTable.FilterMenu tooltip="Filter" /> - <DataTable.SortingMenu tooltip="Sort" /> - <DataTable.Search placeholder="Search..." /> - </div> - </DataTable.Toolbar> - <DataTable.Table /> - <DataTable.Pagination /> - </DataTable> - </Container> - </SingleColumnLayout> -) -``` - -You render the `DataTable` component and pass the `table` instance as a prop. In the `DataTable` component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table. - -Lastly, export the component and the UI widget's configuration at the end of the file: - -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" - -// ... - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - -If you start your Medusa application and go to `localhost:9000/app/custom`, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities. - -### Full Example Code - -```tsx title="src/admin/routes/custom/page.tsx" -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" -import { - Badge, - createDataTableColumnHelper, - createDataTableFilterHelper, - DataTable, - DataTableFilteringState, - DataTablePaginationState, - DataTableSortingState, - Heading, - useDataTable, -} from "@medusajs/ui" -import { useQuery } from "@tanstack/react-query" -import { SingleColumnLayout } from "../../layouts/single-column" -import { sdk } from "../../lib/config" -import { useMemo, useState } from "react" -import { Container } from "../../components/container" -import { HttpTypes, ProductStatus } from "@medusajs/framework/types" - -const columnHelper = createDataTableColumnHelper<HttpTypes.AdminProduct>() - -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 ( - <Badge color={status === "published" ? "green" : "grey"} size="xsmall"> - {status === "published" ? "Published" : "Draft"} - </Badge> - ) - }, - }), -] - -const filterHelper = createDataTableFilterHelper<HttpTypes.AdminProduct>() - -const filters = [ - filterHelper.accessor("status", { - type: "select", - label: "Status", - options: [ - { - label: "Published", - value: "published", - }, - { - label: "Draft", - value: "draft", - }, - ], - }), -] - -const limit = 15 - -const CustomPage = () => { - const [pagination, setPagination] = useState<DataTablePaginationState>({ - pageSize: limit, - pageIndex: 0, - }) - const [search, setSearch] = useState<string>("") - const [filtering, setFiltering] = useState<DataTableFilteringState>({}) - const [sorting, setSorting] = useState<DataTableSortingState | null>(null) +## Example - const offset = useMemo(() => { - return pagination.pageIndex * limit - }, [pagination]) - const statusFilters = useMemo(() => { - return (filtering.status || []) as ProductStatus - }, [filtering]) +Use that `Container` component in any widget or UI route. - const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list({ - limit, - offset, - q: search, - status: statusFilters, - order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, - }), - queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], - }) +For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: - const table = useDataTable({ - columns, - data: data?.products || [], - getRowId: (row) => row.id, - rowCount: data?.count || 0, - isLoading, - pagination: { - state: pagination, - onPaginationChange: setPagination, - }, - search: { - state: search, - onSearchChange: setSearch, - }, - filtering: { - state: filtering, - onFilteringChange: setFiltering, - }, - filters, - sorting: { - // Pass the pagination state and updater to the table instance - state: sorting, - onSortingChange: setSorting, - }, - }) +```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 ( - <SingleColumnLayout> - <Container> - <DataTable instance={table}> - <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center"> - <Heading>Products</Heading> - <div className="flex gap-2"> - <DataTable.FilterMenu tooltip="Filter" /> - <DataTable.SortingMenu tooltip="Sort" /> - <DataTable.Search placeholder="Search..." /> - </div> - </DataTable.Toolbar> - <DataTable.Table /> - <DataTable.Pagination /> - </DataTable> - </Container> - </SingleColumnLayout> + <Container> + <Header title="Product Widget" /> + </Container> ) } -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, +export const config = defineWidgetConfig({ + zone: "product.details.before", }) -export default CustomPage +export default ProductWidget ``` +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. + # JSON View - Admin Components @@ -29617,448 +30478,12 @@ const ProductWidget = () => { export const config = defineWidgetConfig({ zone: "product.details.before", -}) - -export default ProductWidget -``` - -This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`. - - -# 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. - - - -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" - -export type HeadingProps = { - title: string - subtitle?: string - actions?: ( - { - type: "button", - props: React.ComponentProps<typeof Button> - link?: LinkProps - } | - { - type: "action-menu" - props: ActionMenuProps - } | - { - type: "custom" - children: React.ReactNode - } - )[] -} - -export const Header = ({ - title, - subtitle, - actions = [], -}: HeadingProps) => { - return ( - <div className="flex items-center justify-between px-6 py-4"> - <div> - <Heading level="h2">{title}</Heading> - {subtitle && ( - <Text className="text-ui-fg-subtle" size="small"> - {subtitle} - </Text> - )} - </div> - {actions.length > 0 && ( - <div className="flex items-center justify-center gap-x-2"> - {actions.map((action, index) => ( - <> - {action.type === "button" && ( - <Button - {...action.props} - size={action.props.size || "small"} - key={index} - > - <> - {action.props.children} - {action.link && <Link {...action.link} />} - </> - </Button> - )} - {action.type === "action-menu" && ( - <ActionMenu {...action.props} /> - )} - {action.type === "custom" && action.children} - </> - ))} - </div> - )} - </div> - ) -} -``` - -The `Header` component shows a title, and optionally a subtitle and action buttons. - -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. - -It accepts the following props: - -- title: (\`string\`) The section's title. -- subtitle: (\`string\`) The section's subtitle. -- actions: (\`object\[]\`) An array of actions to show. - - - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add. - - \- If its value is \`button\`, it'll show a button that can have a link or an on-click action. - - \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions. - - \- If its value is \`custom\`, you can pass any React nodes to render. - - - props: (object) - - - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions. - -*** - -## Example - -Use the `Header` 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 { Container } from "../components/container" -import { Header } from "../components/header" - -const ProductWidget = () => { - return ( - <Container> - <Header - title="Product Widget" - subtitle="This is my custom product widget" - actions={[ - { - type: "button", - props: { - children: "Click me", - variant: "secondary", - onClick: () => { - alert("You clicked the button.") - }, - }, - }, - ]} - /> - </Container> - ) -} - -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. - - -# Table - Admin Components - -If you're using [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0), it's recommended to use the [Data Table](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/data-table/index.html.md) component instead as it provides features for sorting, filtering, pagination, and more with a simpler API. - -You can use the `Table` component from Medusa UI to display data in a table. It's mostly recommended for simpler tables. - -To create a component that shows a table with pagination, create the file `src/admin/components/table.tsx` with the following content: - -```tsx title="src/admin/components/table.tsx" -import { useMemo } from "react" -import { Table as UiTable } from "@medusajs/ui" - -export type TableProps = { - columns: { - key: string - label?: string - render?: (value: unknown) => React.ReactNode - }[] - data: Record<string, unknown>[] - pageSize: number - count: number - currentPage: number - setCurrentPage: (value: number) => void -} - -export const Table = ({ - columns, - data, - pageSize, - count, - currentPage, - setCurrentPage, -}: TableProps) => { - const pageCount = useMemo(() => { - return Math.ceil(count / pageSize) - }, [data, pageSize]) - - const canNextPage = useMemo(() => { - return currentPage < pageCount - 1 - }, [currentPage, pageCount]) - const canPreviousPage = useMemo(() => { - return currentPage - 1 >= 0 - }, [currentPage]) - - const nextPage = () => { - if (canNextPage) { - setCurrentPage(currentPage + 1) - } - } - - const previousPage = () => { - if (canPreviousPage) { - setCurrentPage(currentPage - 1) - } - } - - return ( - <div className="flex h-full flex-col overflow-hidden !border-t-0"> - <UiTable> - <UiTable.Header> - <UiTable.Row> - {columns.map((column, index) => ( - <UiTable.HeaderCell key={index}> - {column.label || column.key} - </UiTable.HeaderCell> - ))} - </UiTable.Row> - </UiTable.Header> - <UiTable.Body> - {data.map((item, index) => { - const rowIndex = "id" in item ? item.id as string : index - return ( - <UiTable.Row key={rowIndex}> - {columns.map((column, index) => ( - <UiTable.Cell key={`${rowIndex}-${index}`}> - <> - {column.render && column.render(item[column.key])} - {!column.render && ( - <>{item[column.key] as string}</> - )} - </> - </UiTable.Cell> - ))} - </UiTable.Row> - ) - })} - </UiTable.Body> - </UiTable> - <UiTable.Pagination - count={count} - pageSize={pageSize} - pageIndex={currentPage} - pageCount={pageCount} - canPreviousPage={canPreviousPage} - canNextPage={canNextPage} - previousPage={previousPage} - nextPage={nextPage} - /> - </div> - ) -} -``` - -The `Table` component uses the component from the [UI package](https://docs.medusajs.com/ui/components/table/index.html.md), with additional styling and rendering of data. - -It accepts the following props: - -- columns: (\`object\[]\`) The table's columns. - - - key: (\`string\`) The column's key in the passed \`data\` - - - label: (\`string\`) The column's label shown in the table. If not provided, the \`key\` is used. - - - render: (\`(value: unknown) => React.ReactNode\`) By default, the data is shown as-is in the table. You can use this function to change how the value is rendered. The function receives the value is a parameter and returns a React node. -- data: (\`Record\<string, unknown>\[]\`) The data to show in the table for the current page. The keys of each object should be in the \`columns\` array. -- pageSize: (\`number\`) The number of items to show per page. -- count: (\`number\`) The total number of items. -- currentPage: (\`number\`) A zero-based index indicating the current page's number. -- setCurrentPage: (\`(value: number) => void\`) A function used to change the current page. - -*** - -## Example - -Use the `Table` 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 { StatusBadge } from "@medusajs/ui" -import { Table } from "../components/table" -import { useState } from "react" -import { Container } from "../components/container" - -const ProductWidget = () => { - const [currentPage, setCurrentPage] = useState(0) - - return ( - <Container> - <Table - columns={[ - { - key: "name", - label: "Name", - }, - { - key: "is_enabled", - label: "Status", - render: (value: unknown) => { - const isEnabled = value as boolean - - return ( - <StatusBadge color={isEnabled ? "green" : "grey"}> - {isEnabled ? "Enabled" : "Disabled"} - </StatusBadge> - ) - }, - }, - ]} - data={[ - { - name: "John", - is_enabled: true, - }, - { - name: "Jane", - is_enabled: false, - }, - ]} - pageSize={2} - count={2} - currentPage={currentPage} - setCurrentPage={setCurrentPage} - /> - </Container> - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -This widget also uses the [Container](../container.mdx) custom component. - -*** - -## Example With Data Fetching - -This section shows you how to use the `Table` component when fetching data from the Medusa application's API routes. - -Assuming you've set up the JS SDK as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), create the UI route `src/admin/routes/custom/page.tsx` with the following content: - -```tsx title="src/admin/routes/custom/page.tsx" collapsibleLines="1-10" expandButtonLabel="Show Imports" highlights={tableExampleHighlights} -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" -import { useQuery } from "@tanstack/react-query" -import { SingleColumnLayout } from "../../layouts/single-column" -import { Table } from "../../components/table" -import { sdk } from "../../lib/config" -import { useMemo, useState } from "react" -import { Container } from "../../components/container" -import { Header } from "../../components/header" - -const CustomPage = () => { - const [currentPage, setCurrentPage] = useState(0) - const limit = 15 - const offset = useMemo(() => { - return currentPage * limit - }, [currentPage]) - - const { data } = useQuery({ - queryFn: () => sdk.admin.product.list({ - limit, - offset, - }), - queryKey: [["products", limit, offset]], - }) - - // TODO display table -} - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - -In the `CustomPage` component, you define: - -- A state variable `currentPage` that stores the current page of the table. -- A `limit` variable, indicating how many items to retrieve per page -- An `offset` memoized variable indicating how many items to skip before the retrieved items. It's calculated as a multiplication of `currentPage` and `limit`. - -Then, you use `useQuery` from [Tanstack Query](https://tanstack.com/query/latest) to retrieve products using the JS SDK. You pass `limit` and `offset` as query parameters, and you set the `queryKey`, which is used for caching and revalidation, to be based on the key `products`, along with the current limit and offset. So, whenever the `offset` variable changes, the request is sent again to retrieve the products of the current page. - -You can change the query to send a request to a custom API route as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk#send-requests-to-custom-routes/index.html.md). - -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. - -`useQuery` returns an object containing `data`, which holds the response fields including the products and pagination fields. - -Then, to display the table, replace the `TODO` with the following: - -```tsx -return ( - <SingleColumnLayout> - <Container> - <Header title="Products" /> - {data && ( - <Table - columns={[ - { - key: "id", - label: "ID", - }, - { - key: "title", - label: "Title", - }, - ]} - data={data.products as any} - pageSize={data.limit} - count={data.count} - currentPage={currentPage} - setCurrentPage={setCurrentPage} - /> - )} - </Container> - </SingleColumnLayout> -) -``` - -Aside from the `Table` component, this UI route also uses the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md), [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. - -If `data` isn't `undefined`, you display the `Table` component passing it the following props: +}) -- `columns`: The columns to show. You only show the product's ID and title. -- `data`: The rows of the table. You pass it the `products` property of `data`. -- `pageSize`: The maximum number of items per page. You pass it the `count` property of `data`. -- `currentPage` and `setCurrentPage`: The current page and the function to change it. +export default ProductWidget +``` -To test it out, log into the Medusa Admin and open `http://localhost:9000/app/custom`. You'll find a table of products with pagination. +This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`. # Section Row - Admin Components @@ -30153,562 +30578,180 @@ 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. -# 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. - -*** - -## Create Form - -In this section, you'll build a form component to create an item of a resource. - -### Full Component - -```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<zod.infer<typeof schema>>({ - defaultValues: { - name: "", - }, - }) - - const handleSubmit = form.handleSubmit(({ name }) => { - // TODO submit to backend - console.log(name) - }) - - return ( - <FocusModal> - <FocusModal.Trigger asChild> - <Button>Create</Button> - </FocusModal.Trigger> - <FocusModal.Content> - <FormProvider {...form}> - <form - onSubmit={handleSubmit} - className="flex h-full flex-col overflow-hidden" - > - <FocusModal.Header> - <div className="flex items-center justify-end gap-x-2"> - <FocusModal.Close asChild> - <Button size="small" variant="secondary"> - Cancel - </Button> - </FocusModal.Close> - <Button type="submit" size="small"> - Save - </Button> - </div> - </FocusModal.Header> - <FocusModal.Body> - <div className="flex flex-1 flex-col items-center overflow-y-auto"> - <div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16"> - <div> - <Heading className="capitalize"> - Create Item - </Heading> - </div> - <div className="grid grid-cols-2 gap-4"> - <Controller - control={form.control} - name="name" - render={({ field }) => { - return ( - <div className="flex flex-col space-y-2"> - <div className="flex items-center gap-x-1"> - <Label size="small" weight="plus"> - Name - </Label> - </div> - <Input {...field} /> - </div> - ) - }} - /> - </div> - </div> - </div> - </FocusModal.Body> - </form> - </FormProvider> - </FocusModal.Content> - </FocusModal> - ) -} -``` - -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. - -### Create Validation Schema - -In `src/admin/components/create-form.tsx`, create a validation schema with Zod for the form's fields: - -```tsx title="src/admin/components/create-form.tsx" -import * as zod from "zod" - -const schema = zod.object({ - name: zod.string(), -}) -``` - -The form in this guide is simple, it only has a required `name` field, which is a string. - -### Initialize Form - -Next, you'll initialize the form using `react-hook-form`. - -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" - -// validation schema... - -export const CreateForm = () => { - const form = useForm<zod.infer<typeof schema>>({ - defaultValues: { - name: "", - }, - }) - - const handleSubmit = form.handleSubmit(({ name }) => { - // TODO submit to backend - console.log(name) - }) - - // TODO render form -} -``` - -You create the `CreateForm` 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. - -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. - -### Render Components - -You'll now add a `return` statement that renders the focus modal where the form is shown. - -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 ( - <FocusModal> - <FocusModal.Trigger asChild> - <Button>Create</Button> - </FocusModal.Trigger> - <FocusModal.Content> - <FormProvider {...form}> - <form - onSubmit={handleSubmit} - className="flex h-full flex-col overflow-hidden" - > - <FocusModal.Header> - <div className="flex items-center justify-end gap-x-2"> - <FocusModal.Close asChild> - <Button size="small" variant="secondary"> - Cancel - </Button> - </FocusModal.Close> - <Button type="submit" size="small"> - Save - </Button> - </div> - </FocusModal.Header> - <FocusModal.Body> - <div className="flex flex-1 flex-col items-center overflow-y-auto"> - <div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16"> - <div> - <Heading className="capitalize"> - Create Item - </Heading> - </div> - <div className="grid grid-cols-2 gap-4"> - <Controller - control={form.control} - name="name" - render={({ field }) => { - return ( - <div className="flex flex-col space-y-2"> - <div className="flex items-center gap-x-1"> - <Label size="small" weight="plus"> - Name - </Label> - </div> - <Input {...field} /> - </div> - ) - }} - /> - </div> - </div> - </div> - </FocusModal.Body> - </form> - </FormProvider> - </FocusModal.Content> - </FocusModal> - ) -} -``` - -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. - -In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event. - -In the `FocusModal.Header` component, you add buttons to save or cancel the form submission. - -Finally, you render the form's components inside the `FocusModal.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`. +# Table - Admin Components -### Use Create Form Component +If you're using [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0), it's recommended to use the [Data Table](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/data-table/index.html.md) component instead as it provides features for sorting, filtering, pagination, and more with a simpler API. -You can use the `CreateForm` component in your widget or UI route. +You can use the `Table` component from Medusa UI to display data in a table. It's mostly recommended for simpler tables. -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: +To create a component that shows a table with pagination, create the file `src/admin/components/table.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" +```tsx title="src/admin/components/table.tsx" +import { useMemo } from "react" +import { Table as UiTable } from "@medusajs/ui" -const ProductWidget = () => { - return ( - <Container> - <Header - title="Items" - actions={[ - { - type: "custom", - children: <CreateForm />, - }, - ]} - /> - </Container> - ) +export type TableProps = { + columns: { + key: string + label?: string + render?: (value: unknown) => React.ReactNode + }[] + data: Record<string, unknown>[] + pageSize: number + count: number + currentPage: number + setCurrentPage: (value: number) => void } -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -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 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" +export const Table = ({ + columns, + data, + pageSize, + count, + currentPage, + setCurrentPage, +}: TableProps) => { + const pageCount = useMemo(() => { + return Math.ceil(count / pageSize) + }, [data, pageSize]) -const schema = zod.object({ - name: zod.string(), -}) + const canNextPage = useMemo(() => { + return currentPage < pageCount - 1 + }, [currentPage, pageCount]) + const canPreviousPage = useMemo(() => { + return currentPage - 1 >= 0 + }, [currentPage]) -export const EditForm = () => { - const form = useForm<zod.infer<typeof schema>>({ - defaultValues: { - name: "", - }, - }) + const nextPage = () => { + if (canNextPage) { + setCurrentPage(currentPage + 1) + } + } - const handleSubmit = form.handleSubmit(({ name }) => { - // TODO submit to backend - console.log(name) - }) + const previousPage = () => { + if (canPreviousPage) { + setCurrentPage(currentPage - 1) + } + } return ( - <Drawer> - <Drawer.Trigger asChild> - <Button>Edit Item</Button> - </Drawer.Trigger> - <Drawer.Content> - <FormProvider {...form}> - <form - onSubmit={handleSubmit} - className="flex flex-1 flex-col overflow-hidden" - > - <Drawer.Header> - <Heading className="capitalize"> - Edit Item - </Heading> - </Drawer.Header> - <Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto"> - <Controller - control={form.control} - name="name" - render={({ field }) => { - return ( - <div className="flex flex-col space-y-2"> - <div className="flex items-center gap-x-1"> - <Label size="small" weight="plus"> - Name - </Label> - </div> - <Input {...field} /> - </div> - ) - }} - /> - </Drawer.Body> - <Drawer.Footer> - <div className="flex items-center justify-end gap-x-2"> - <Drawer.Close asChild> - <Button size="small" variant="secondary"> - Cancel - </Button> - </Drawer.Close> - <Button size="small" type="submit"> - Save - </Button> - </div> - </Drawer.Footer> - </form> - </FormProvider> - </Drawer.Content> - </Drawer> - ) -} -``` - -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 - -In `src/admin/components/edit-form.tsx`, create a validation schema with Zod for the form's fields: - -```tsx title="src/admin/components/edit-form.tsx" -import * as zod from "zod" - -const schema = zod.object({ - name: zod.string(), -}) -``` - -The form in this guide is simple, it only has a required `name` field, which is a string. - -### Initialize Form - -Next, you'll initialize the form using `react-hook-form`. - -Add to `src/admin/components/edit-form.tsx` the following: - -```tsx title="src/admin/components/edit-form.tsx" -// other imports... -import { useForm } from "react-hook-form" - -// validation schema... - -export const EditForm = () => { - const form = useForm<zod.infer<typeof schema>>({ - defaultValues: { - name: "", - }, - }) - - const handleSubmit = form.handleSubmit(({ name }) => { - // TODO submit to backend - console.log(name) - }) - - // TODO render form + <div className="flex h-full flex-col overflow-hidden !border-t-0"> + <UiTable> + <UiTable.Header> + <UiTable.Row> + {columns.map((column, index) => ( + <UiTable.HeaderCell key={index}> + {column.label || column.key} + </UiTable.HeaderCell> + ))} + </UiTable.Row> + </UiTable.Header> + <UiTable.Body> + {data.map((item, index) => { + const rowIndex = "id" in item ? item.id as string : index + return ( + <UiTable.Row key={rowIndex}> + {columns.map((column, index) => ( + <UiTable.Cell key={`${rowIndex}-${index}`}> + <> + {column.render && column.render(item[column.key])} + {!column.render && ( + <>{item[column.key] as string}</> + )} + </> + </UiTable.Cell> + ))} + </UiTable.Row> + ) + })} + </UiTable.Body> + </UiTable> + <UiTable.Pagination + count={count} + pageSize={pageSize} + pageIndex={currentPage} + pageCount={pageCount} + canPreviousPage={canPreviousPage} + canNextPage={canNextPage} + previousPage={previousPage} + nextPage={nextPage} + /> + </div> + ) } ``` -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. - -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. - -### Render Components - -You'll now add a `return` statement that renders the drawer where the form is shown. - -Replace `// TODO render form` with the following: - -```tsx title="src/admin/components/edit-form.tsx" -// other imports... -import { - Drawer, - Heading, - Label, - Input, - Button, -} from "@medusajs/ui" -import { - FormProvider, - Controller, -} from "react-hook-form" - -export const EditForm = () => { - // ... +The `Table` component uses the component from the [UI package](https://docs.medusajs.com/ui/components/table/index.html.md), with additional styling and rendering of data. - return ( - <Drawer> - <Drawer.Trigger asChild> - <Button>Edit Item</Button> - </Drawer.Trigger> - <Drawer.Content> - <FormProvider {...form}> - <form - onSubmit={handleSubmit} - className="flex flex-1 flex-col overflow-hidden" - > - <Drawer.Header> - <Heading className="capitalize"> - Edit Item - </Heading> - </Drawer.Header> - <Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto"> - <Controller - control={form.control} - name="name" - render={({ field }) => { - return ( - <div className="flex flex-col space-y-2"> - <div className="flex items-center gap-x-1"> - <Label size="small" weight="plus"> - Name - </Label> - </div> - <Input {...field} /> - </div> - ) - }} - /> - </Drawer.Body> - <Drawer.Footer> - <div className="flex items-center justify-end gap-x-2"> - <Drawer.Close asChild> - <Button size="small" variant="secondary"> - Cancel - </Button> - </Drawer.Close> - <Button size="small" type="submit"> - Save - </Button> - </div> - </Drawer.Footer> - </form> - </FormProvider> - </Drawer.Content> - </Drawer> - ) -} -``` +It accepts the following props: -You render a drawer, with a trigger button to open it. +- columns: (\`object\[]\`) The table's columns. -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. + - key: (\`string\`) The column's key in the passed \`data\` -In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event. + - label: (\`string\`) The column's label shown in the table. If not provided, the \`key\` is used. -You render the form's components inside the `Drawer.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`. + - render: (\`(value: unknown) => React.ReactNode\`) By default, the data is shown as-is in the table. You can use this function to change how the value is rendered. The function receives the value is a parameter and returns a React node. +- data: (\`Record\<string, unknown>\[]\`) The data to show in the table for the current page. The keys of each object should be in the \`columns\` array. +- pageSize: (\`number\`) The number of items to show per page. +- count: (\`number\`) The total number of items. +- currentPage: (\`number\`) A zero-based index indicating the current page's number. +- setCurrentPage: (\`(value: number) => void\`) A function used to change the current page. -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 the `Table` 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 { StatusBadge } from "@medusajs/ui" +import { Table } from "../components/table" +import { useState } from "react" import { Container } from "../components/container" -import { Header } from "../components/header" -import { EditForm } from "../components/edit-form" const ProductWidget = () => { + const [currentPage, setCurrentPage] = useState(0) + return ( <Container> - <Header - title="Items" - actions={[ + <Table + columns={[ { - type: "custom", - children: <EditForm />, + key: "name", + label: "Name", + }, + { + key: "is_enabled", + label: "Status", + render: (value: unknown) => { + const isEnabled = value as boolean + + return ( + <StatusBadge color={isEnabled ? "green" : "grey"}> + {isEnabled ? "Enabled" : "Disabled"} + </StatusBadge> + ) + }, + }, + ]} + data={[ + { + name: "John", + is_enabled: true, + }, + { + name: "Jane", + is_enabled: false, }, ]} + pageSize={2} + count={2} + currentPage={currentPage} + setCurrentPage={setCurrentPage} /> </Container> ) @@ -30721,9 +30764,108 @@ 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](../container.mdx) custom component. -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. +*** + +## Example With Data Fetching + +This section shows you how to use the `Table` component when fetching data from the Medusa application's API routes. + +Assuming you've set up the JS SDK as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), create the UI route `src/admin/routes/custom/page.tsx` with the following content: + +```tsx title="src/admin/routes/custom/page.tsx" collapsibleLines="1-10" expandButtonLabel="Show Imports" highlights={tableExampleHighlights} +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" +import { useQuery } from "@tanstack/react-query" +import { SingleColumnLayout } from "../../layouts/single-column" +import { Table } from "../../components/table" +import { sdk } from "../../lib/config" +import { useMemo, useState } from "react" +import { Container } from "../../components/container" +import { Header } from "../../components/header" + +const CustomPage = () => { + const [currentPage, setCurrentPage] = useState(0) + const limit = 15 + const offset = useMemo(() => { + return currentPage * limit + }, [currentPage]) + + const { data } = useQuery({ + queryFn: () => sdk.admin.product.list({ + limit, + offset, + }), + queryKey: [["products", limit, offset]], + }) + + // TODO display table +} + +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) + +export default CustomPage +``` + +In the `CustomPage` component, you define: + +- A state variable `currentPage` that stores the current page of the table. +- A `limit` variable, indicating how many items to retrieve per page +- An `offset` memoized variable indicating how many items to skip before the retrieved items. It's calculated as a multiplication of `currentPage` and `limit`. + +Then, you use `useQuery` from [Tanstack Query](https://tanstack.com/query/latest) to retrieve products using the JS SDK. You pass `limit` and `offset` as query parameters, and you set the `queryKey`, which is used for caching and revalidation, to be based on the key `products`, along with the current limit and offset. So, whenever the `offset` variable changes, the request is sent again to retrieve the products of the current page. + +You can change the query to send a request to a custom API route as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk#send-requests-to-custom-routes/index.html.md). + +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. + +`useQuery` returns an object containing `data`, which holds the response fields including the products and pagination fields. + +Then, to display the table, replace the `TODO` with the following: + +```tsx +return ( + <SingleColumnLayout> + <Container> + <Header title="Products" /> + {data && ( + <Table + columns={[ + { + key: "id", + label: "ID", + }, + { + key: "title", + label: "Title", + }, + ]} + data={data.products as any} + pageSize={data.limit} + count={data.count} + currentPage={currentPage} + setCurrentPage={setCurrentPage} + /> + )} + </Container> + </SingleColumnLayout> +) +``` + +Aside from the `Table` component, this UI route also uses the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md), [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. + +If `data` isn't `undefined`, you display the `Table` component passing it the following props: + +- `columns`: The columns to show. You only show the product's ID and title. +- `data`: The rows of the table. You pass it the `products` property of `data`. +- `pageSize`: The maximum number of items per page. You pass it the `count` property of `data`. +- `currentPage` and `setCurrentPage`: The current page and the function to change it. + +To test it out, log into the Medusa Admin and open `http://localhost:9000/app/custom`. You'll find a table of products with pagination. # Service Factory Reference @@ -30831,124 +30973,6 @@ 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). -# list Method - Service Factory Reference - -This method retrieves a list of records. - -## Retrieve List of Records - -```ts -const posts = await postModuleService.listPosts() -``` - -If no parameters are passed, the method returns an array of the first `15` records. - -*** - -## Filter Records - -```ts -const posts = await postModuleService.listPosts({ - 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 of the first `15` records matching the 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 = await postModuleService.listPosts({}, { - relations: ["author"], -}) -``` - -### 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 - -The method returns an array of the first `15` records matching the filters. - -*** - -## Select Properties - -```ts -const posts = await postModuleService.listPosts({}, { - select: ["id", "name"], -}) -``` - -### 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. - -### Returns - -The method returns an array of the first `15` records matching the filters. - -*** - -## Paginate Relations - -```ts -const posts = await postModuleService.listPosts({}, { - take: 20, - skip: 10, -}) -``` - -### 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 of records. The number of records is less than or equal to `take`'s value. - -*** - -## Sort Records - -```ts -const posts = await postModuleService.listPosts({}, { - order: { - name: "ASC", - }, -}) -``` - -### 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 of the first `15` records matching the filters. - - # 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). @@ -31093,142 +31117,6 @@ By default, all of the record's properties are retrieved. To select specific one The method returns the record as an object. -# 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 - -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. - -*** - -## Select Properties - -```ts -const [posts, count] = await postModuleService.listAndCountPosts({}, { - select: ["id", "name"], -}) -``` - -### 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. - -### 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. - -*** - -## Paginate Relations - -```ts -const [posts, count] = await postModuleService.listAndCountPosts({}, { - take: 20, - skip: 10, -}) -``` - -### 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. - -*** - -## Sort Records - -```ts -const [posts, count] = await postModuleService.listAndCountPosts({}, { - order: { - name: "ASC", - }, -}) -``` - -### 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. - - # softDelete Method - Service Factory Reference This method soft deletes one or more records of the data model. @@ -31582,6 +31470,260 @@ To use an `or` condition, pass to the filter object the `$or` property, whose va 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 + +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. + +*** + +## Select Properties + +```ts +const [posts, count] = await postModuleService.listAndCountPosts({}, { + select: ["id", "name"], +}) +``` + +### 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. + +### 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. + +*** + +## Paginate Relations + +```ts +const [posts, count] = await postModuleService.listAndCountPosts({}, { + take: 20, + skip: 10, +}) +``` + +### 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. + +*** + +## Sort Records + +```ts +const [posts, count] = await postModuleService.listAndCountPosts({}, { + order: { + name: "ASC", + }, +}) +``` + +### 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. + + +# list Method - Service Factory Reference + +This method retrieves a list of records. + +## Retrieve List of Records + +```ts +const posts = await postModuleService.listPosts() +``` + +If no parameters are passed, the method returns an array of the first `15` records. + +*** + +## Filter Records + +```ts +const posts = await postModuleService.listPosts({ + 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 of the first `15` records matching the 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 = await postModuleService.listPosts({}, { + relations: ["author"], +}) +``` + +### 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 + +The method returns an array of the first `15` records matching the filters. + +*** + +## Select Properties + +```ts +const posts = await postModuleService.listPosts({}, { + select: ["id", "name"], +}) +``` + +### 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. + +### Returns + +The method returns an array of the first `15` records matching the filters. + +*** + +## Paginate Relations + +```ts +const posts = await postModuleService.listPosts({}, { + take: 20, + skip: 10, +}) +``` + +### 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 of records. The number of records is less than or equal to `take`'s value. + +*** + +## Sort Records + +```ts +const posts = await postModuleService.listPosts({}, { + order: { + name: "ASC", + }, +}) +``` + +### 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 of the first `15` records matching the filters. + + <H3 className="!mt-0">Just Getting Started?</H3> @@ -32023,6 +32165,125 @@ How to install and setup Medusa UI. +# Medusa Admin Extension + +How to install and use Medusa UI for building Admin extensions. + +## Installation + +*** + +The `@medusajs/ui` package is a already installed as a dependency of the `@medusajs/admin` package. Due to this you can simply import the package and use it in your local Admin extensions. + +If you are building a Admin extension as part of a Medusa plugin, you can install the package as a dependency of your plugin. + +```bash +npm install @medusajs/ui +``` + +## Configuration + +*** + +The configuration of the UI package is handled by the `@medusajs/admin` package. Therefore, you do not need to any additional configuration to use the UI package in your Admin extensions. + + +# Standalone Project + +How to install and use Medusa UI in a standalone project. + +## Installation + +*** + +Medusa UI is a React UI library and while it's intended for usage within Medusa projects, it can also be used in any React project. + +### Install Medusa UI + +Install the React UI library with the following command: + +```bash +npm install @medusajs/ui +``` + +### Configuring Tailwind CSS + +The components are styled using Tailwind CSS, and in order to use them, you will need to install Tailwind CSS in your project as well. +For more information on how to install Tailwind CSS, please refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs/installation). + +All of the classes used for Medusa UI are shipped as a Tailwind CSS customization. +You can install it with the following command: + +```bash +npm install @medusajs/ui-preset +``` + +After you have installed Tailwind CSS and the Medusa UI preset, you need to add the following to your `tailwind.config.js`file: + +```tsx +module.exports = { + presets: [require("@medusajs/ui-preset")], + // ... +} +``` + +In order for the styles to be applied correctly to the components, you will also need to ensure that +`@medusajs/ui` is included in the content field of your `tailwind.config.js` file: + +```tsx +module.exports = { + content: [ + // ... + "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}", + ], + // ... +} +``` + +If you are working within a monorepo, you may need to add the path to the `@medusajs/ui` package in your `tailwind.config.js` like so: + +```tsx +const path = require("path") + +const uiPath = path.resolve( + require.resolve("@medusajs/ui"), + "../..", + "\*_/_.{js,jsx,ts,tsx}" +) + +module.exports = { + content: [ + // ... + uiPath, + ], + // ... +} + +``` + +## Start building + +*** + +You are now ready to start building your application with Medusa UI. You can import the components like so: + +```tsx +import { Button, Drawer } from "@medusajs/ui" +``` + +## Updating UI Packages + +*** + +Medusa's design-system packages, including `@medusajs/ui`, `@medusajs/ui-preset`, and `@medusajs/ui-icons`, are versioned independently. However, they're still part of the latest Medusa release. So, you can browse the [release notes](https://github.com/medusajs/medusa/releases) to see if there are any breaking changes to these packages. + +To update these packages, update their version in your `package.json` file and re-install dependencies. For example: + +```bash +npm install @medusajs/ui +``` + + # Alert A component for displaying important messages. @@ -38418,125 +38679,6 @@ If you're using the `Tooltip` component in a project other than the Medusa Admin - disableHoverableContent: (boolean) When \`true\`, trying to hover the content will result in the tooltip closing as the pointer leaves the trigger. -# Medusa Admin Extension - -How to install and use Medusa UI for building Admin extensions. - -## Installation - -*** - -The `@medusajs/ui` package is a already installed as a dependency of the `@medusajs/admin` package. Due to this you can simply import the package and use it in your local Admin extensions. - -If you are building a Admin extension as part of a Medusa plugin, you can install the package as a dependency of your plugin. - -```bash -npm install @medusajs/ui -``` - -## Configuration - -*** - -The configuration of the UI package is handled by the `@medusajs/admin` package. Therefore, you do not need to any additional configuration to use the UI package in your Admin extensions. - - -# Standalone Project - -How to install and use Medusa UI in a standalone project. - -## Installation - -*** - -Medusa UI is a React UI library and while it's intended for usage within Medusa projects, it can also be used in any React project. - -### Install Medusa UI - -Install the React UI library with the following command: - -```bash -npm install @medusajs/ui -``` - -### Configuring Tailwind CSS - -The components are styled using Tailwind CSS, and in order to use them, you will need to install Tailwind CSS in your project as well. -For more information on how to install Tailwind CSS, please refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs/installation). - -All of the classes used for Medusa UI are shipped as a Tailwind CSS customization. -You can install it with the following command: - -```bash -npm install @medusajs/ui-preset -``` - -After you have installed Tailwind CSS and the Medusa UI preset, you need to add the following to your `tailwind.config.js`file: - -```tsx -module.exports = { - presets: [require("@medusajs/ui-preset")], - // ... -} -``` - -In order for the styles to be applied correctly to the components, you will also need to ensure that -`@medusajs/ui` is included in the content field of your `tailwind.config.js` file: - -```tsx -module.exports = { - content: [ - // ... - "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}", - ], - // ... -} -``` - -If you are working within a monorepo, you may need to add the path to the `@medusajs/ui` package in your `tailwind.config.js` like so: - -```tsx -const path = require("path") - -const uiPath = path.resolve( - require.resolve("@medusajs/ui"), - "../..", - "\*_/_.{js,jsx,ts,tsx}" -) - -module.exports = { - content: [ - // ... - uiPath, - ], - // ... -} - -``` - -## Start building - -*** - -You are now ready to start building your application with Medusa UI. You can import the components like so: - -```tsx -import { Button, Drawer } from "@medusajs/ui" -``` - -## Updating UI Packages - -*** - -Medusa's design-system packages, including `@medusajs/ui`, `@medusajs/ui-preset`, and `@medusajs/ui-icons`, are versioned independently. However, they're still part of the latest Medusa release. So, you can browse the [release notes](https://github.com/medusajs/medusa/releases) to see if there are any breaking changes to these packages. - -To update these packages, update their version in your `package.json` file and re-install dependencies. For example: - -```bash -npm install @medusajs/ui -``` - - # clx Utility function for working with classNames. diff --git a/www/apps/resources/app/commerce-modules/auth/authentication-route/page.mdx b/www/apps/resources/app/commerce-modules/auth/authentication-route/page.mdx index 7edc4f7328f07..75bcdf2a3a1c4 100644 --- a/www/apps/resources/app/commerce-modules/auth/authentication-route/page.mdx +++ b/www/apps/resources/app/commerce-modules/auth/authentication-route/page.mdx @@ -339,8 +339,9 @@ If the authentication is successful, the request returns a `201` response code. 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?token=123 +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" @@ -360,9 +361,15 @@ 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`. -#### Query Parameters +#### Pass Token in Authorization Header -The route accepts a `token` query parameter, which is the token generated using the [Generate Reset Password Token route](#generate-reset-password-token-route). +<Note> + +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. + +</Note> + +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. ### Request Body Parameters diff --git a/www/apps/resources/app/storefront-development/customers/reset-password/page.mdx b/www/apps/resources/app/storefront-development/customers/reset-password/page.mdx index b7d517c9aef4f..bb321e3f05595 100644 --- a/www/apps/resources/app/storefront-development/customers/reset-password/page.mdx +++ b/www/apps/resources/app/storefront-development/customers/reset-password/page.mdx @@ -185,11 +185,12 @@ export const resetPasswordFetchHighlights = [ return } - fetch(`http://localhost:9000/auth/customer/emailpass/update?token=${token}`, { + fetch(`http://localhost:9000/auth/customer/emailpass/update`, { credentials: "include", method: "POST", headers: { "Content-Type": "application/json", + "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ email, @@ -249,11 +250,12 @@ export const resetPasswordHighlights = [ } setLoading(true) - fetch(`http://localhost:9000/auth/customer/emailpass/update?token=${token}`, { + fetch(`http://localhost:9000/auth/customer/emailpass/update`, { credentials: "include", method: "POST", headers: { "Content-Type": "application/json", + "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ email, @@ -289,4 +291,10 @@ export const resetPasswordHighlights = [ In this example, you receive the `token` and `email` from the page's query parameters. -Then, when the form that has the password field is submitted, you send a request to the `http://localhost:9000/auth/customer/emailpass/update` API route. You pass it the token as a query parameter, and the email and password in the request body. +Then, when the form that has the password field is submitted, you send a request to the `http://localhost:9000/auth/customer/emailpass/update` API route. You pass it the token as in the Authorization header as a bearer token, and the email and password in the request body. + +<Note> + +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. + +</Note> diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index b6912d767826f..1f5903e597744 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -1,7 +1,7 @@ export const generatedEditDates = { "app/commerce-modules/auth/auth-providers/emailpass/page.mdx": "2025-01-13T11:31:35.361Z", "app/commerce-modules/auth/auth-providers/page.mdx": "2024-10-08T07:27:21.859Z", - "app/commerce-modules/auth/authentication-route/page.mdx": "2025-01-13T11:31:17.572Z", + "app/commerce-modules/auth/authentication-route/page.mdx": "2025-03-04T09:13:45.919Z", "app/commerce-modules/auth/examples/page.mdx": "2024-10-15T15:02:13.794Z", "app/commerce-modules/auth/module-options/page.mdx": "2025-01-07T12:47:35.073Z", "app/commerce-modules/auth/page.mdx": "2025-01-09T13:41:05.476Z", @@ -2108,7 +2108,7 @@ export const generatedEditDates = { "app/admin-components/layouts/two-column/page.mdx": "2024-10-07T11:16:10.092Z", "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": "2024-12-19T16:32:00.724Z", + "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/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", From d58eb3b75c6e20cee54b0b51b05611a17e83b477 Mon Sep 17 00:00:00 2001 From: Shahed Nasser <shahednasser@gmail.com> Date: Tue, 4 Mar 2025 12:15:31 +0200 Subject: [PATCH 2/4] small fixes --- .../api-routes/middlewares/page.mdx | 10 +- www/apps/book/generated/edit-dates.mjs | 2 +- www/apps/book/public/llms-full.txt | 20144 ++++++++-------- 3 files changed, 10080 insertions(+), 10076 deletions(-) diff --git a/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx index 4018265cf07f4..1d7f3083ce340 100644 --- a/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx +++ b/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx @@ -66,7 +66,7 @@ export default defineMiddlewares({ The `defineMiddlewares` function accepts a middleware configurations object that has the property `routes`. `routes`'s value is an array of middleware route objects, each having the following properties: - `matcher`: a string or regular expression indicating the API route path to apply the middleware on. The regular expression must be compatible with [path-to-regexp](https://github.com/pillarjs/path-to-regexp). -- `middlewares`: An array of middleware functions. +- `middlewares`: An array of global and route middleware functions. 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`. @@ -91,7 +91,7 @@ Received a request! ## How to Create a Route Middleware? -In the previous section, you learned how to create a global middleware. You define the route middleware in the same way, but you specify an additional property `method`. Its value is one or more HTTP methods to apply the middleware to. +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: @@ -317,6 +317,8 @@ And the route middlewares are sorted into the following order before they're reg 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. + ### Middlewares and Route Execution Order When a request is sent to an API route, the global middlewares are executed first, then the route middlewares, and finally the route handler. @@ -363,6 +365,6 @@ The global middleware runs first, then the route middleware, and finally the rou ## Overriding Middlewares -A middleware can never override an existing middleware. Instead, middlewares are added to the end of the middleware stack. +A middleware can not override an existing middleware. Instead, middlewares are added to the end of the middleware stack. -For example, if you define a custom validation middleware on an existing route, such as `validateAndTransformBody`, then both the original and the custom validation middleware will run. +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. diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index 69b3c48678273..f7cd9eae862ca 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -53,7 +53,7 @@ export const generatedEditDates = { "app/learn/fundamentals/admin/tips/page.mdx": "2025-02-24T09:52:06.901Z", "app/learn/fundamentals/api-routes/cors/page.mdx": "2024-12-09T13:04:04.357Z", "app/learn/fundamentals/admin/ui-routes/page.mdx": "2025-02-24T09:35:11.752Z", - "app/learn/fundamentals/api-routes/middlewares/page.mdx": "2025-03-04T09:46:35.475Z", + "app/learn/fundamentals/api-routes/middlewares/page.mdx": "2025-03-04T10:15:17.128Z", "app/learn/fundamentals/modules/isolation/page.mdx": "2024-12-09T11:02:38.087Z", "app/learn/fundamentals/data-models/configure-properties/page.mdx": "2024-10-21T13:30:21.368Z", "app/learn/fundamentals/data-models/index/page.mdx": "2024-10-21T13:30:21.368Z", diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index 92b3931972e1b..dc084c7a8dd69 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -847,6 +847,113 @@ Medusa's Testing Framework works for integration tests only. You can write unit The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests. +# Build Custom Features + +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. + +To build a custom feature in Medusa, you need three main tools: + +- [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. + + + +*** + +## 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. + + +# 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. + + +# 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. + + +# 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. + + + +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). + + # 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. @@ -1148,90 +1255,6 @@ 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. -# Build Custom Features - -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. - -To build a custom feature in Medusa, you need three main tools: - -- [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. - - - -*** - -## 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. - - -# 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. - - -# 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. - - - -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). - - # 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. @@ -1256,29 +1279,6 @@ In the Development Resources documentation, you'll also find step-by-step guides Refer to the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation to learn more. -# 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's Architecture In this chapter, you'll learn about the architectural layers in Medusa. @@ -1427,6 +1427,65 @@ Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.m 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. +# API Routes + +In this chapter, you’ll learn what API Routes are and how to create them. + +## What is an API Route? + +An API Route is an endpoint. It exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems. + +The Medusa core application provides a set of admin and store API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities. + +*** + +## How to Create an API Route? + +An API Route is created in a TypeScript or JavaScript file under the `src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`. + + + +Each file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). + +For example, to create a `GET` API Route at `/hello-world`, create the file `src/api/hello-world/route.ts` with the following content: + +```ts title="src/api/hello-world/route.ts" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "[GET] Hello world!", + }) +} +``` + +### Test API Route + +To test the API route above, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, send a `GET` request to the `/hello-world` API Route: + +```bash +curl http://localhost:9000/hello-world +``` + +*** + +## When to Use API Routes + +You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application. + + # Custom CLI Scripts In this chapter, you'll learn how to create and execute custom scripts from Medusa's CLI tool. @@ -1497,65 +1556,6 @@ npx medusa exec ./src/scripts/my-script.ts arg1 arg2 ``` -# API Routes - -In this chapter, you’ll learn what API Routes are and how to create them. - -## What is an API Route? - -An API Route is an endpoint. It exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems. - -The Medusa core application provides a set of admin and store API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities. - -*** - -## How to Create an API Route? - -An API Route is created in a TypeScript or JavaScript file under the `src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`. - - - -Each file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). - -For example, to create a `GET` API Route at `/hello-world`, create the file `src/api/hello-world/route.ts` with the following content: - -```ts title="src/api/hello-world/route.ts" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "[GET] Hello world!", - }) -} -``` - -### Test API Route - -To test the API route above, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, send a `GET` request to the `/hello-world` API Route: - -```bash -curl http://localhost:9000/hello-world -``` - -*** - -## When to Use API Routes - -You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application. - - # Environment Variables In this chapter, you'll learn how environment variables are loaded in Medusa. @@ -1887,6 +1887,63 @@ Medusa provides two Event Modules out of the box: 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. + +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + +## What is a Plugin? + +A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). + +Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project. + + + +Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md). + +*** + +## Plugin vs Module + +A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase. + +A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it. + +For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service. + +- You want to reuse your Medusa customizations across multiple projects. +- You want to share your Medusa customizations with the community. + +- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow. + +*** + +## How to Create a Plugin? + +The next chapter explains how you can create and publish a plugin. + +*** + +## Plugin Guides and Resources + +For more resources and guides related to plugins, refer to the [Resources documentation](https://docs.medusajs.com/resources/plugins/index.html.md). + + # Module Link In this chapter, you’ll learn what a module link is. @@ -2011,63 +2068,6 @@ export default defineLink( In this example, when a product is deleted, its linked `myCustom` record is also deleted. -# Plugins - -In this chapter, you'll learn what a plugin is in Medusa. - -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). - -## What is a Plugin? - -A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). - -Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project. - - - -Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md). - -*** - -## Plugin vs Module - -A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase. - -A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it. - -For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service. - -- You want to reuse your Medusa customizations across multiple projects. -- You want to share your Medusa customizations with the community. - -- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow. - -*** - -## How to Create a Plugin? - -The next chapter explains how you can create and publish a plugin. - -*** - -## Plugin Guides and Resources - -For more resources and guides related to plugins, refer to the [Resources documentation](https://docs.medusajs.com/resources/plugins/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. - - # Modules In this chapter, you’ll learn about modules and how to create them. @@ -2462,6 +2462,207 @@ In the scheduled job function, you execute the `syncProductToErpWorkflow` by inv The next time you start the Medusa application, it will run this job every day at midnight. +# Write Integration Tests + +In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests. + +### 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. + +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... + }, +}) + +jest.setTimeout(60 * 1000) +``` + +The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`. + +`testSuite`'s value is a function that defines the tests to run. The function accepts as a parameter an object that has the following properties: + +- `api`: a set of utility methods used to send requests to the Medusa application. It has the following methods: + - `get`: Send a `GET` request to an API route. + - `post`: Send a `POST` request to an API route. + - `delete`: Send a `DELETE` request to an API route. +- `getContainer`: a function that retrieves the Medusa Container. Use the `getContainer().resolve` method to resolve resources from the Medusa Container. + +The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). + +### 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) +``` + +*** + +### Run Tests + +Run the following command to run your tests: + +```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 + +Refer to [this reference in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. + +*** + +## Database Used in Tests + +The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. + +To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md). + +*** + +## Example Integration Tests + +The next chapters provide examples of writing integration tests for API routes and workflows. + + +# Write Tests for Modules + +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) + +## moduleIntegrationTestRunner Utility + +`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. + +For example, assuming you have a `hello` module, create a test file at `src/modules/hello/__tests__/service.spec.ts`: + +```ts title="src/modules/hello/__tests__/service.spec.ts" +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import { HELLO_MODULE } from ".." +import HelloModuleService from "../service" +import MyCustom from "../models/my-custom" + +moduleIntegrationTestRunner<HelloModuleService>({ + moduleName: HELLO_MODULE, + moduleModels: [MyCustom], + resolve: "./src/modules/hello", + testSuite: ({ service }) => { + // TODO write tests + }, +}) + +jest.setTimeout(60 * 1000) +``` + +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 Tests + +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. + +*** + +## 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<HelloModuleService>({ + moduleOptions: { + apiKey: "123", + }, + // ... +}) +``` + +*** + +## Write Tests for Modules without Data Models + +If your module doesn't have a data model, pass a dummy model in the `moduleModels` property. + +For example: + +```ts +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import HelloModuleService from "../service" +import { model } from "@medusajs/framework/utils" + +const DummyModel = model.define("dummy_model", { + id: model.id().primaryKey(), +}) + +moduleIntegrationTestRunner<HelloModuleService>({ + moduleModels: [DummyModel], + // ... +}) + +jest.setTimeout(60 * 1000) +``` + +*** + +### Other Options and Inputs + +Refer to [this reference in the Development Resources documentation](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 [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). + + # Workflows In this chapter, you’ll learn about workflows and how to define and execute them. @@ -2924,361 +3125,142 @@ Now that you have brands in your Medusa application, you want to associate a bra In the next chapters, you'll learn how to build associations between data models defined in different modules. -# Guide: Implement Brand Module - -In this chapter, you'll build a Brand Module that adds a `brand` table to the database and provides data-management features for it. - -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 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. - -Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -## 1. Create Module Directory - -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. - - - -*** - -## 2. Create Data Model - -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. - -Learn more about data models in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#1-create-data-model/index.html.md). - -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: - - +# Guide: Create Brand Workflow -```ts title="src/modules/brand/models/brand.ts" -import { model } from "@medusajs/framework/utils" +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. -export const Brand = model.define("brand", { - id: model.id().primaryKey(), - name: model.text(), -}) -``` +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. -You create a `Brand` data model which has an `id` primary key property, and a `name` text property. +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. -You define the data model using the `define` method of the DML. It accepts two parameters: +Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). -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. +### Prerequisites -Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/property-types/index.html.md). +- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) *** -## 3. Create Module Service - -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. - -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). +## 1. Create createBrandStep -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: +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 - +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: -```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, -}) { +```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" +export type CreateBrandStepInput = { + name: string } -export default BrandModuleService -``` - -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. - -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). - -Find a reference of all generated methods in [this guide](https://docs.medusajs.com/resources/service-factory-reference/index.html.md). - -*** - -## 4. Export Module Definition - -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. - -So, to export the Brand Module's definition, create the file `src/modules/brand/index.ts` with the following content: - - - -```ts title="src/modules/brand/index.ts" -import { Module } from "@medusajs/framework/utils" -import BrandModuleService from "./service" - -export const BRAND_MODULE = "brand" - -export default Module(BRAND_MODULE, { - service: BrandModuleService, -}) -``` - -You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: - -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. - -*** - -## 5. Add Module to Medusa's Configurations - -To start using your module, you must add it to Medusa's configurations in `medusa-config.ts`. - -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="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/brand", - }, - ], -}) -``` - -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). - -*** - -## 6. Generate and Run Migrations - -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. - -Learn more about migrations in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#5-generate-migrations/index.html.md). +export const createBrandStep = createStep( + "create-brand-step", + async (input: CreateBrandStepInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) -[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: + const brand = await brandModuleService.createBrands(input) -```bash -npx medusa db:generate brand -npx medusa db:migrate + return new StepResponse(brand, brand.id) + } +) ``` -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. - -*** - -## Next Step: Create Brand Workflow - -The Brand Module now creates a `brand` table in the database and provides a class to manage its records. - -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. - - -# Write Tests for Modules - -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) - -## moduleIntegrationTestRunner Utility - -`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. - -For example, assuming you have a `hello` module, create a test file at `src/modules/hello/__tests__/service.spec.ts`: - -```ts title="src/modules/hello/__tests__/service.spec.ts" -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import { HELLO_MODULE } from ".." -import HelloModuleService from "../service" -import MyCustom from "../models/my-custom" +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. -moduleIntegrationTestRunner<HelloModuleService>({ - moduleName: HELLO_MODULE, - moduleModels: [MyCustom], - resolve: "./src/modules/hello", - testSuite: ({ service }) => { - // TODO write tests - }, -}) +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. -jest.setTimeout(60 * 1000) -``` +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. -The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties: +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. -- `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. +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). -The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service. +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. -The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property. +### Add Compensation Function to Step -The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). +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). -## Run Tests +To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`: -Run the following command to run your module integration tests: +```ts title="src/workflows/create-brand.ts" +export const createBrandStep = createStep( + // ... + async (id: string, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) -```bash npm2yarn -npm run test:integration:modules + await brandModuleService.deleteBrands(id) + } +) ``` -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. - -*** - -## Pass Module Options - -If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter. +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. -For example: +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. -```ts -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import HelloModuleService from "../service" +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). -moduleIntegrationTestRunner<HelloModuleService>({ - moduleOptions: { - apiKey: "123", - }, - // ... -}) -``` +So, if an error occurs during the workflow's execution, the brand that was created by the step is deleted to maintain data consistency. *** -## 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: +## 2. Create createBrandWorkflow -```ts -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import HelloModuleService from "../service" -import { model } from "@medusajs/framework/utils" +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. -const DummyModel = model.define("dummy_model", { - id: model.id().primaryKey(), -}) +Add the following content in the same `src/workflows/create-brand.ts` file: -moduleIntegrationTestRunner<HelloModuleService>({ - moduleModels: [DummyModel], +```ts title="src/workflows/create-brand.ts" +// other imports... +import { // ... -}) - -jest.setTimeout(60 * 1000) -``` - -*** - -### Other Options and Inputs - -Refer to [this reference in the Development Resources documentation](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 [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). - - -# Write Integration Tests - -In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests. - -### 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. - -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... - }, -}) - -jest.setTimeout(60 * 1000) -``` - -The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`. - -`testSuite`'s value is a function that defines the tests to run. The function accepts as a parameter an object that has the following properties: - -- `api`: a set of utility methods used to send requests to the Medusa application. It has the following methods: - - `get`: Send a `GET` request to an API route. - - `post`: Send a `POST` request to an API route. - - `delete`: Send a `DELETE` request to an API route. -- `getContainer`: a function that retrieves the Medusa Container. Use the `getContainer().resolve` method to resolve resources from the Medusa Container. - -The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). - -### 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) -``` + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -*** +// ... -### Run Tests +type CreateBrandWorkflowInput = { + name: string +} -Run the following command to run your tests: +export const createBrandWorkflow = createWorkflow( + "create-brand", + (input: CreateBrandWorkflowInput) => { + const brand = createBrandStep(input) -```bash npm2yarn -npm run test:integration + return new WorkflowResponse(brand) + } +) ``` -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. - -*** +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. -## Other Options and Inputs +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. -Refer to [this reference in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. +A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor. *** -## Database Used in Tests - -The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. - -To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md). - -*** +## Next Steps: Expose Create Brand API Route -## Example Integration Tests +You now have a `createBrandWorkflow` that you can execute to create a brand. -The next chapters provide examples of writing integration tests for API routes and workflows. +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. # Create Brands UI Route in Admin @@ -3653,711 +3635,523 @@ Your customizations often span across systems, where you need to retrieve data o 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. -# Guide: Add Product's Brand Widget in Admin +# Guide: Implement Brand Module -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. +In this chapter, you'll build a Brand Module that adds a `brand` table to the database and provides data-management features for it. -### Prerequisites +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. -- [Brands linked to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) +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. -## 1. Initialize JS SDK +Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -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. +## 1. Create Module Directory -So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content: +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. - + -```ts title="src/admin/lib/sdk.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", - }, +## 2. Create Data Model + +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. + +Learn more about data models in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#1-create-data-model/index.html.md). + +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: + + + +```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(), }) ``` -You initialize the SDK passing it the following options: - -- `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. +You create a `Brand` data model which has an `id` primary key property, and a `name` text property. -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 define the data model using the `define` method of the DML. It accepts two parameters: -You can now use the SDK to send requests to the Medusa server. +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. -Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). +Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/property-types/index.html.md). *** -## 2. Add Widget to Product Details Page - -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. +## 3. Create Module Service -Learn more about widgets in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/widgets/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. -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: +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). -```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" +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: -type AdminProductBrand = AdminProduct & { - brand?: { - id: string - name: string - } -} + -const ProductBrandWidget = ({ - data: product, -}: DetailWidgetProps<AdminProduct>) => { - 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 +```ts title="src/modules/brand/service.ts" highlights={serviceHighlights} +import { MedusaService } from "@medusajs/framework/utils" +import { Brand } from "./models/brand" - return ( - <Container className="divide-y p-0"> - <div className="flex items-center justify-between px-6 py-4"> - <div> - <Heading level="h2">Brand</Heading> - </div> - </div> - <div - className={clx( - `text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4` - )} - > - <Text size="small" weight="plus" leading="compact"> - Name - </Text> +class BrandModuleService extends MedusaService({ + Brand, +}) { - <Text - size="small" - leading="compact" - className="whitespace-pre-line text-pretty" - > - {brandName || "-"} - </Text> - </div> - </Container> - ) } -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductBrandWidget +export default BrandModuleService ``` -A widget's file must export: +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. -- 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. +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. -Since the widget is injected at the top of the product details page, the widget receives the product's details as a parameter. +You'll use these methods in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). -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. +Find a reference of all generated methods in [this guide](https://docs.medusajs.com/resources/service-factory-reference/index.html.md). -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. +*** -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. +## 4. Export Module Definition -*** +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. -## Test it Out +So, to export the Brand Module's definition, create the file `src/modules/brand/index.ts` with the following content: -To test out your widget, start the Medusa application: + -```bash npm2yarn -npm run dev +```ts title="src/modules/brand/index.ts" +import { Module } from "@medusajs/framework/utils" +import BrandModuleService from "./service" + +export const BRAND_MODULE = "brand" + +export default Module(BRAND_MODULE, { + service: BrandModuleService, +}) ``` -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. +You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: - +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. *** -## Admin Components Guides +## 5. Add Module to Medusa's Configurations -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. +To start using your module, you must add it to Medusa's configurations in `medusa-config.ts`. -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. +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="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/brand", + }, + ], +}) +``` + +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). *** -## Next Chapter: Add UI Route for Brands +## 6. Generate and Run Migrations -In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users. +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. +Learn more about migrations in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#5-generate-migrations/index.html.md). -# Guide: Create Brand Workflow +[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: -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. +```bash +npx medusa db:generate brand +npx medusa db:migrate +``` -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 `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. -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. +*** -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). +## Next Step: Create Brand Workflow -### Prerequisites +The Brand Module now creates a `brand` table in the database and provides a class to manage its records. -- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +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. -*** -## 1. Create createBrandStep +# Guide: Add Product's Brand Widget in Admin -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 +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. -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: +### Prerequisites - +- [Brands linked to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) -```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" +## 1. Initialize JS SDK -export type CreateBrandStepInput = { - name: string -} +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. -export const createBrandStep = createStep( - "create-brand-step", - async (input: CreateBrandStepInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content: - const brand = await brandModuleService.createBrands(input) + - return new StepResponse(brand, brand.id) - } -) +```ts title="src/admin/lib/sdk.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", + }, +}) ``` -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. +You initialize the SDK passing it the following options: -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. +- `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 [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. +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). -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. +You can now use the SDK to send requests to the Medusa server. -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). +Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). -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. +*** -### Add Compensation Function to Step +## 2. Add Widget to Product Details Page -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. +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 the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). +Learn more about widgets in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md). -To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`: +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: -```ts title="src/workflows/create-brand.ts" -export const createBrandStep = createStep( - // ... - async (id: string, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) + - await brandModuleService.deleteBrands(id) +```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" + +type AdminProductBrand = AdminProduct & { + brand?: { + id: string + name: string } -) -``` +} -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 ProductBrandWidget = ({ + data: product, +}: DetailWidgetProps<AdminProduct>) => { + 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 -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. + return ( + <Container className="divide-y p-0"> + <div className="flex items-center justify-between px-6 py-4"> + <div> + <Heading level="h2">Brand</Heading> + </div> + </div> + <div + className={clx( + `text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4` + )} + > + <Text size="small" weight="plus" leading="compact"> + Name + </Text> -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). + <Text + size="small" + leading="compact" + className="whitespace-pre-line text-pretty" + > + {brandName || "-"} + </Text> + </div> + </Container> + ) +} -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 const config = defineWidgetConfig({ + zone: "product.details.before", +}) -*** +export default ProductBrandWidget +``` -## 2. Create createBrandWorkflow +A widget's file must export: -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. +- 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. -Add the following content in the same `src/workflows/create-brand.ts` file: +Since the widget is injected at the top of the product details page, the widget receives the product's details as a parameter. -```ts title="src/workflows/create-brand.ts" -// other imports... -import { - // ... - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +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. -// ... +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. -type CreateBrandWorkflowInput = { - name: string -} +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. -export const createBrandWorkflow = createWorkflow( - "create-brand", - (input: CreateBrandWorkflowInput) => { - const brand = createBrandStep(input) +*** - return new WorkflowResponse(brand) - } -) -``` +## Test it Out -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. +To test out your widget, start the Medusa application: -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. +```bash npm2yarn +npm run dev +``` -A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor. +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. + + *** -## Next Steps: Expose Create Brand API Route +## Admin Components Guides -You now have a `createBrandWorkflow` that you can execute to create a brand. +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. -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. +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. +*** -# Guide: Define Module Link Between Brand and Product +## Next Chapter: Add UI Route for Brands -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. +In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users. -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. +# Guide: Query Product's Brands -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 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. -Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). +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 -- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +- [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) -## 1. Define Link +*** -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. +## Approach 1: Retrieve Brands in Existing API Routes -So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content: +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. - +Learn more about selecting fields and relations in the [API Reference](https://docs.medusajs.com/api/admin#select-fields-and-relations). -```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" +For example, send the following request to retrieve the list of products with their brands: -export default defineLink( - { - linkable: ProductModule.linkable.product, - isList: true, - }, - BrandModule.linkable.brand -) +```bash +curl 'http://localhost:9000/admin/products?fields=+brand.*' \ +--header 'Authorization: Bearer {token}' ``` -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. +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 `defineLink` function accepts two parameters of the same type, which is either: +Any product that is linked to a brand will have a `brand` property in its object: -- 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. +```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 + } +} +``` -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. +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. *** -## 2. Sync the Link to the Database - -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: +## Approach 2: Use Query to Retrieve Linked Records -```bash -npx medusa db:migrate -``` +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. -This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link. +Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). -You can also run the `npx medusa db:sync-links` to just sync module links without running migrations. +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" -## Next Steps: Extend Create Product Flow +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve("query") + + const { data: brands } = await query.graph({ + entity: "brand", + fields: ["*", "products.*"], + }) -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. + res.json({ brands }) +} +``` +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: -# Guide: Sync Brands from Medusa to CMS +- `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. -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. +`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. -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. +### Test it Out -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. +To test the API route out, send a `GET` request to `/admin/brands`: -Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). +```bash +curl 'http://localhost:9000/admin/brands' \ +-H 'Authorization: Bearer {token}' +``` -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. +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). -### Prerequisites +This returns the brands in your store with their linked products. For example: -- [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) +```json title="Example Response" +{ + "brands": [ + { + "id": "123", + // ... + "products": [ + { + "id": "prod_123", + // ... + } + ] + } + ] +} +``` -## 1. Emit Event in createBrandWorkflow +*** -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. +## Summary -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`: +By following the examples of the previous chapters, you: -```ts title="src/workflows/create-brand.ts" highlights={eventHighlights} -// other imports... -import { - emitEventStep, -} from "@medusajs/medusa/core-flows" +- 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. -// ... +*** -export const createBrandWorkflow = createWorkflow( - "create-brand", - (input: CreateBrandInput) => { - // ... +## Next Steps: Customize Medusa Admin - emitEventStep({ - eventName: "brand.created", - data: { - id: brand.id, - }, - }) +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. - return new WorkflowResponse(brand) - } -) -``` +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. -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. +# Guide: Extend Create Product Flow -You'll learn how to handle this event in a later step. +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. -*** +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. -## 2. Create Sync to Third-Party System Workflow +So, in this chapter, to extend the create product flow and associate a brand with a product, you will: -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. +- 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`. -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. +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). -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). +### Prerequisites -You'll create a `syncBrandToSystemWorkflow` that has two steps: +- [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) -- `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 +## 1. Consume the productsCreated Hook -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: +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. - +Learn more about the workflow hooks in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). -```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" +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. -type SyncBrandToCmsStepInput = { - brand: InferTypeOf<typeof Brand> -} +To consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content: -const syncBrandToCmsStep = createStep( - "sync-brand-to-cms", - async ({ brand }: SyncBrandToCmsStepInput, { container }) => { - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) + - await cmsModuleService.createBrand(brand) +```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" - return new StepResponse(null, brand.id) - }, - async (id, { container }) => { - if (!id) { - return +createProductsWorkflow.hooks.productsCreated( + (async ({ products, additional_data }, { container }) => { + if (!additional_data?.brand_id) { + return new StepResponse([], []) } - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) + 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) - await cmsModuleService.deleteBrand(id) - } + // TODO link brand to product + }) ) ``` -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. +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: -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. +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. -Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). +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. -### Create Workflow +### Link Brand to Product -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: +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/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" +Learn more about Link in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md). -// ... - -type SyncBrandToCmsWorkflowInput = { - id: string -} - -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, - }, - }) - - syncBrandToCmsStep({ - brand: brands[0], - } as SyncBrandToCmsStepInput) - - return new WorkflowResponse({}) - } -) -``` - -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. - -You'll execute this workflow in the subscriber next. - -Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). - -*** - -## 3. Handle brand.created Event - -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. - -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/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 config: SubscriberConfig = { - event: "brand.created", -} -``` - -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. - -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. - -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. - -Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). - -*** - -## Test it Out - -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. - -First, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -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" -}' -``` - -Make sure to replace the email and password with your admin user's credentials. - -Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). - -Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: - -```bash -curl -X POST 'http://localhost:9000/admin/brands' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data '{ - "name": "Acme" -}' -``` - -This 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" -``` - -*** - -## Next Chapter: Sync Brand from Third-Party CMS to Medusa - -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. - - -# Guide: Extend Create Product Flow - -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. - -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: - -- 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`. - -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 - -- [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) - -*** - -## 1. Consume the productsCreated Hook - -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. - -Learn more about the workflow hooks in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). - -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 consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content: - - - -```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" - -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 - }) -) -``` - -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. - -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. - -### Link Brand to Product - -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. - -Learn more about Link in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md). - -To use Link in the `productsCreated` hook, replace the `TODO` with the following: +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") @@ -4499,276 +4293,527 @@ In the Medusa application's logs, you'll find the message `Linked brand to produ 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. -# Guide: Query Product's Brands +# Guide: Define Module Link Between Brand and Product -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. +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. -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. +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. -### Prerequisites +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. -- [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 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. -*** +Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). -## Approach 1: Retrieve Brands in Existing API Routes +### Prerequisites -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. +- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) -Learn more about selecting fields and relations in the [API Reference](https://docs.medusajs.com/api/admin#select-fields-and-relations). +## 1. Define Link -For example, send the following request to retrieve the list of products with their brands: +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. -```bash -curl 'http://localhost:9000/admin/products?fields=+brand.*' \ ---header 'Authorization: Bearer {token}' -``` +So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content: -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). + -Any product that is linked to a brand will have a `brand` property in its object: +```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" -```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 - } -} +export default defineLink( + { + linkable: ProductModule.linkable.product, + isList: true, + }, + BrandModule.linkable.brand +) ``` -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. - -*** +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. -## Approach 2: Use Query to Retrieve Linked Records +The `defineLink` function accepts two parameters of the same type, which is either: -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. +- 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. -Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). +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. -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" +## 2. Sync the Link to the Database -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve("query") - - const { data: brands } = await query.graph({ - entity: "brand", - fields: ["*", "products.*"], - }) +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: - res.json({ brands }) -} +```bash +npx medusa db:migrate ``` -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: - -- `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. +This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link. -`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. +You can also run the `npx medusa db:sync-links` to just sync module links without running migrations. -### Test it Out +*** -To test the API route out, send a `GET` request to `/admin/brands`: +## Next Steps: Extend Create Product Flow -```bash -curl 'http://localhost:9000/admin/brands' \ --H 'Authorization: Bearer {token}' -``` +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. -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). -This returns the brands in your store with their linked products. For example: +# Guide: Sync Brands from Medusa to CMS -```json title="Example Response" -{ - "brands": [ - { - "id": "123", - // ... - "products": [ - { - "id": "prod_123", - // ... - } - ] - } - ] -} -``` +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. -*** +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. -## Summary +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. -By following the examples of the previous chapters, you: +Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/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. +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. -*** +### Prerequisites -## Next Steps: Customize Medusa Admin +- [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) -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. +## 1. Emit Event in createBrandWorkflow -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. +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. +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`: -# Guide: Schedule Syncing Brands from CMS +```ts title="src/workflows/create-brand.ts" highlights={eventHighlights} +// other imports... +import { + emitEventStep, +} from "@medusajs/medusa/core-flows" -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. +// ... -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. +export const createBrandWorkflow = createWorkflow( + "create-brand", + (input: CreateBrandInput) => { + // ... -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. + emitEventStep({ + eventName: "brand.created", + data: { + id: brand.id, + }, + }) -Learn more about scheduled jobs in [this chapter](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). + return new WorkflowResponse(brand) + } +) +``` -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. +The `emitEventStep` accepts an object parameter having two properties: -### Prerequisites +- `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. -- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) +You'll learn how to handle this event in a later step. *** -## 1. Implement Syncing Workflow +## 2. Create Sync to Third-Party System Workflow -You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job. +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). -This workflow will have three steps: +You'll create a `syncBrandToSystemWorkflow` that has two steps: -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. +- `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. -### retrieveBrandsFromCmsStep +### syncBrandToCmsStep -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: +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: - + -```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" +```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" -const retrieveBrandsFromCmsStep = createStep( - "retrieve-brands-from-cms", - async (_, { container }) => { - const cmsModuleService: CmsModuleService = container.resolve( - CMS_MODULE - ) - - const brands = await cmsModuleService.retrieveBrands() - - return new StepResponse(brands) - } -) -``` - -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. - -### 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: - -```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" - -// ... - -type CreateBrand = { - name: string -} - -type CreateBrandsInput = { - brands: CreateBrand[] +type SyncBrandToCmsStepInput = { + brand: InferTypeOf<typeof Brand> } -export const createBrandsStep = createStep( - "create-brands-step", - async (input: CreateBrandsInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +const syncBrandToCmsStep = createStep( + "sync-brand-to-cms", + async ({ brand }: SyncBrandToCmsStepInput, { container }) => { + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) - const brands = await brandModuleService.createBrands(input.brands) + await cmsModuleService.createBrand(brand) - return new StepResponse(brands, brands) + return new StepResponse(null, brand.id) }, - async (brands, { container }) => { - if (!brands) { + async (id, { container }) => { + if (!id) { return } - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) - await brandModuleService.deleteBrands(brands.map((brand) => brand.id)) + await cmsModuleService.deleteBrand(id) } ) ``` -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 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. -The step passes the created brands to the compensation function, which deletes those brands if an error occurs during the workflow's execution. +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). -### Update Brands Step +### Create Workflow -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: +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: + +```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" -```ts title="src/workflows/sync-brands-from-cms.ts" highlights={updateBrandsHighlights} // ... -type UpdateBrand = { +type SyncBrandToCmsWorkflowInput = { id: string - name: string -} - -type UpdateBrandsInput = { - brands: UpdateBrand[] } -export const updateBrandsStep = createStep( - "update-brands-step", - async ({ brands }: UpdateBrandsInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +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, + }, + }) + + syncBrandToCmsStep({ + brand: brands[0], + } as SyncBrandToCmsStepInput) + + return new WorkflowResponse({}) + } +) +``` + +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. + +You'll execute this workflow in the subscriber next. + +Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). + +*** + +## 3. Handle brand.created Event + +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. + +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/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 config: SubscriberConfig = { + event: "brand.created", +} +``` + +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. + +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. + +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. + +Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). + +*** + +## Test it Out + +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. + +First, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +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" +}' +``` + +Make sure to replace the email and password with your admin user's credentials. + +Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). + +Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: + +```bash +curl -X POST 'http://localhost:9000/admin/brands' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "name": "Acme" +}' +``` + +This 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" +``` + +*** + +## Next Chapter: Sync Brand from Third-Party CMS to Medusa + +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. + + +# Admin Development Constraints + +This chapter lists some constraints of admin widgets and UI routes. + +## Arrow Functions + +Widget and UI route components must be created as arrow functions. + +```ts highlights={arrowHighlights} +// Don't +function ProductWidget() { + // ... +} + +// Do +const ProductWidget = () => { + // ... +} +``` + +*** + +## Widget Zone + +A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. + +```ts highlights={zoneHighlights} +// Don't +export const config = defineWidgetConfig({ + zone: `product.details.before`, +}) + +// Don't +const ZONE = "product.details.after" +export const config = defineWidgetConfig({ + zone: ZONE, +}) + +// Do +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) +``` + + +# Guide: Schedule Syncing Brands from CMS + +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. + +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. + +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. + +### Prerequisites + +- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) + +*** + +## 1. Implement Syncing Workflow + +You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job. + +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). + +This workflow will have three steps: + +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 + +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: + + + +```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 + ) + + const brands = await cmsModuleService.retrieveBrands() + + return new StepResponse(brands) + } +) +``` + +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. + +### 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: + +```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" + +// ... + +type CreateBrand = { + name: string +} + +type CreateBrandsInput = { + brands: CreateBrand[] +} + +export const createBrandsStep = createStep( + "create-brands-step", + async (input: CreateBrandsInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + const brands = await brandModuleService.createBrands(input.brands) + + return new StepResponse(brands, brands) + }, + async (brands, { container }) => { + if (!brands) { + return + } + + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + await brandModuleService.deleteBrands(brands.map((brand) => brand.id)) + } +) +``` + +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. + +The step passes the created brands to the compensation function, which deletes those brands 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). + +### Update Brands Step + +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 +} + +type UpdateBrandsInput = { + brands: UpdateBrand[] +} + +export const updateBrandsStep = createStep( + "update-brands-step", + async ({ brands }: UpdateBrandsInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) const prevUpdatedBrands = await brandModuleService.listBrands({ id: brands.map((brand) => brand.id), @@ -4944,51 +4989,6 @@ By following the previous chapters, you utilized Medusa's framework and orchestr 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. -# Admin Development Constraints - -This chapter lists some constraints of admin widgets and UI routes. - -## Arrow Functions - -Widget and UI route components must be created as arrow functions. - -```ts highlights={arrowHighlights} -// Don't -function ProductWidget() { - // ... -} - -// Do -const ProductWidget = () => { - // ... -} -``` - -*** - -## Widget Zone - -A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. - -```ts highlights={zoneHighlights} -// Don't -export const config = defineWidgetConfig({ - zone: `product.details.before`, -}) - -// Don't -const ZONE = "product.details.after" -export const config = defineWidgetConfig({ - zone: ZONE, -}) - -// Do -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) -``` - - # Guide: Integrate CMS Brand System 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. @@ -5217,407 +5217,135 @@ To check the current environment, Vite exposes two variables: Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode). -# Admin Routing Customizations +# Admin Development Tips -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 find some tips for your admin development. -In this chapter, you'll learn about routing-related customizations that you can use in your admin customizations using React Router. +## Send Requests to API Routes -`react-router-dom` is available in your project by default through the Medusa packages. You don't need to install it separately. +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. -## Link to a Page +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. -To link to a page in your admin customizations, you can use the `Link` component from `react-router-dom`. For example: +First, create the file `src/admin/lib/config.ts` to setup the SDK for use in your customizations: -```tsx title="src/admin/widgets/product-widget.tsx" highlights={highlights} +```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", + }, +}) +``` + +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). + +Learn more about the JS SDK's configurations [this documentation](https://docs.medusajs.com/resources/js-sdk#js-sdk-configurations/index.html.md). + +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: + +### Query + +```tsx title="src/admin/widgets/product-widget.ts" highlights={queryHighlights} import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container } from "@medusajs/ui" -import { Link } from "react-router-dom" +import { Button, Container } from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { sdk } from "../lib/config" +import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" -// The widget const ProductWidget = () => { + const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list(), + queryKey: ["products"], + }) + return ( <Container className="divide-y p-0"> - <Link to={"/orders"}>View Orders</Link> + {isLoading && <span>Loading...</span>} + {data?.products && ( + <ul> + {data.products.map((product) => ( + <li key={product.id}>{product.title}</li> + ))} + </ul> + )} </Container> ) } -// The widget's configurations export const config = defineWidgetConfig({ - zone: "product.details.before", + zone: "product.list.before", }) 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. +### Mutation -*** +```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" -## Admin Route Loader +const ProductWidget = ({ + data: productData, +}: DetailWidgetProps<HttpTypes.AdminProduct>) => { + const { mutateAsync } = useMutation({ + mutationFn: (payload: HttpTypes.AdminUpdateProduct) => + sdk.admin.product.update(productData.id, payload), + onSuccess: () => alert("updated product"), + }) -Route loaders are available starting from Medusa v2.5.1. + const handleUpdate = () => { + mutateAsync({ + title: "New Product Title", + }) + } + + return ( + <Container className="divide-y p-0"> + <Button onClick={handleUpdate}>Update Title</Button> + </Container> + ) +} -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. +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) -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. +export default ProductWidget +``` -For example, consider the following UI route created at `src/admin/routes/custom/page.tsx`: +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). -```tsx title="src/admin/routes/custom/page.tsx" highlights={loaderHighlights} -import { Container, Heading } from "@medusajs/ui" -import { - useLoaderData, -} from "react-router-dom" +### Use Route Loaders for Initial Data -export async function loader() { - // TODO fetch products +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). - return { - products: [], - } -} +*** -const CustomPage = () => { - const { products } = useLoaderData() as Awaited<ReturnType<typeof loader>> +## Global Variables in Admin Customizations - return ( - <div> - <Container className="divide-y p-0"> - <div className="flex items-center justify-between px-6 py-4"> - <Heading level="h2">Products count: {products.length}</Heading> - </div> - </Container> - </div> - ) -} +In your admin customizations, you can use the following global variables: -export default CustomPage -``` +- `__BASE__`: The base path of the Medusa Admin, as set in the [admin.path](https://docs.medusajs.com/resources/references/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/resources/references/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/resources/references/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`. -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. +*** -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. +## Admin Translations -### Route Parameters +The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions. -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`: - -```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={loaderParamHighlights} -import { Container, Heading } from "@medusajs/ui" -import { - useLoaderData, - LoaderFunctionArgs, -} from "react-router-dom" - -export async function loader({ params }: LoaderFunctionArgs) { - const { id } = params - // TODO fetch product by id - - return { - id, - } -} - -const CustomPage = () => { - const { id } = useLoaderData() as Awaited<ReturnType<typeof loader>> - - return ( - <div> - <Container className="divide-y p-0"> - <div className="flex items-center justify-between px-6 py-4"> - <Heading level="h2">Product ID: {id}</Heading> - </div> - </Container> - </div> - ) -} - -export default CustomPage -``` - -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. - -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. - -### 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. - -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. - -*** - -## Other React Router Utilities - -### Route Handles - -Route handles are available starting from Medusa v2.5.1. - -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: - -```tsx title="src/admin/routes/custom/page.tsx" -export const handle = { - sandbox: true, -} -``` - -### 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. - - -# Admin Development Tips - -In this chapter, you'll find some tips for your admin development. - -## Send Requests to API Routes - -To send a request to an API route in the Medusa Application, use 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. - -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. - -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", - }, -}) -``` - -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). - -Learn more about the JS SDK's configurations [this documentation](https://docs.medusajs.com/resources/js-sdk#js-sdk-configurations/index.html.md). - -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: - -### Query - -```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" - -const ProductWidget = () => { - const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list(), - queryKey: ["products"], - }) - - return ( - <Container className="divide-y p-0"> - {isLoading && <span>Loading...</span>} - {data?.products && ( - <ul> - {data.products.map((product) => ( - <li key={product.id}>{product.title}</li> - ))} - </ul> - )} - </Container> - ) -} - -export const config = defineWidgetConfig({ - zone: "product.list.before", -}) - -export default ProductWidget -``` - -### Mutation - -```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" - -const ProductWidget = ({ - data: productData, -}: DetailWidgetProps<HttpTypes.AdminProduct>) => { - const { mutateAsync } = useMutation({ - mutationFn: (payload: HttpTypes.AdminUpdateProduct) => - sdk.admin.product.update(productData.id, payload), - onSuccess: () => alert("updated product"), - }) - - const handleUpdate = () => { - mutateAsync({ - title: "New Product Title", - }) - } - - return ( - <Container className="divide-y p-0"> - <Button onClick={handleUpdate}>Update Title</Button> - </Container> - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -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 - -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). - -*** - -## Global Variables in Admin Customizations - -In your admin customizations, you can use the following global variables: - -- `__BASE__`: The base path of the Medusa Admin, as set in the [admin.path](https://docs.medusajs.com/resources/references/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/resources/references/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/resources/references/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`. - -*** - -## Admin Translations - -The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions. - -Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/resources/contribution-guidelines/admin-translations/index.html.md). - - -# Admin Widgets - -In this chapter, you’ll learn more about widgets and how to use them. - -## What is an Admin Widget? - -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. - -For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. - -*** - -## How to Create a Widget? - -### Prerequisites - -- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) - -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: - - - -```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" - -// The widget -const ProductWidget = () => { - return ( - <Container className="divide-y p-0"> - <div className="flex items-center justify-between px-6 py-4"> - <Heading level="h2">Product Widget</Heading> - </div> - </Container> - ) -} - -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -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. - -The widget component must be created as an arrow function. - -### Test the Widget - -To test out the widget, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, open a product’s details page. You’ll find your custom widget at the top of the page. - -*** - -## Props Passed in Detail Pages - -Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. - -For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: - -```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" -import { - DetailWidgetProps, - AdminProduct, -} from "@medusajs/framework/types" - -// The widget -const ProductWidget = ({ - data, -}: DetailWidgetProps<AdminProduct>) => { - return ( - <Container className="divide-y p-0"> - <div className="flex items-center justify-between px-6 py-4"> - <Heading level="h2"> - Product Widget {data.title} - </Heading> - </div> - </Container> - ) -} - -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -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`. - -*** - -## Injection Zone - -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. - -*** - -## 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. +Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/resources/contribution-guidelines/admin-translations/index.html.md). # Admin UI Routes @@ -5856,192 +5584,388 @@ To build admin customizations that match the Medusa Admin's designs and layouts, For more customizations related to routes, refer to the [Routing Customizations chapter](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md). -# Seed Data with Custom CLI Script +# Admin Routing Customizations -In this chapter, you'll learn how to seed data using a custom CLI script. +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. -## How to Seed Data +In this chapter, you'll learn about routing-related customizations that you can use in your admin customizations using React Router. -To seed dummy data for development or demo purposes, use a custom CLI script. +`react-router-dom` is available in your project by default through the Medusa packages. You don't need to install it separately. -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. +## Link to a Page -### Example: Seed Dummy Products +To link to a page in your admin customizations, you can use the `Link` component from `react-router-dom`. For example: -In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products. +```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" -First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script: +// The widget +const ProductWidget = () => { + return ( + <Container className="divide-y p-0"> + <Link to={"/orders"}>View Orders</Link> + </Container> + ) +} -```bash npm2yarn -npm install --save-dev @faker-js/faker +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget ``` -Then, create the file `src/scripts/demo-products.ts` with the following content: +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. -```ts title="src/scripts/demo-products.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" -import { ExecArgs } from "@medusajs/framework/types" -import { faker } from "@faker-js/faker" -import { - ContainerRegistrationKeys, - Modules, - ProductStatus, -} from "@medusajs/framework/utils" -import { - createInventoryLevelsWorkflow, - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" +*** -export default async function seedDummyProducts({ - container, -}: ExecArgs) { - const salesChannelModuleService = container.resolve( - Modules.SALES_CHANNEL - ) - const logger = container.resolve( - ContainerRegistrationKeys.LOGGER - ) - const query = container.resolve( - ContainerRegistrationKeys.QUERY - ) +## Admin Route Loader - const defaultSalesChannel = await salesChannelModuleService - .listSalesChannels({ - name: "Default Sales Channel", - }) +Route loaders are available starting from Medusa v2.5.1. - const sizeOptions = ["S", "M", "L", "XL"] - const colorOptions = ["Black", "White"] - const currency_code = "eur" - const productsNum = 50 +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. - // TODO seed products -} -``` +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. -So far, in the script, you: +For example, consider the following UI route created at `src/admin/routes/custom/page.tsx`: -- 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. +```tsx title="src/admin/routes/custom/page.tsx" highlights={loaderHighlights} +import { Container, Heading } from "@medusajs/ui" +import { + useLoaderData, +} from "react-router-dom" -Next, replace the `TODO` with the following: +export async function loader() { + // TODO fetch products -```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, - }, - ], + products: [], } -}) - -// TODO seed products -``` - -You generate fifty products using the sales channel and variables you initialized, and using Faker for random data, such as the product's title or images. - -Then, replace the new `TODO` with the following: +} -```ts title="src/scripts/demo-products.ts" -const { result: products } = await createProductsWorkflow(container).run({ - input: { - products: productsData, - }, -}) +const CustomPage = () => { + const { products } = useLoaderData() as Awaited<ReturnType<typeof loader>> -logger.info(`Seeded ${products.length} products.`) + return ( + <div> + <Container className="divide-y p-0"> + <div className="flex items-center justify-between px-6 py-4"> + <Heading level="h2">Products count: {products.length}</Heading> + </div> + </Container> + </div> + ) +} -// TODO add inventory levels +export default CustomPage ``` -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 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. -Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following: +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. -```ts title="src/scripts/demo-products.ts" -logger.info("Seeding inventory levels.") +### Route Parameters -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: ["id"], -}) +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`: -const { data: inventoryItems } = await query.graph({ - entity: "inventory_item", - fields: ["id"], -}) +```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={loaderParamHighlights} +import { Container, Heading } from "@medusajs/ui" +import { + useLoaderData, + LoaderFunctionArgs, +} from "react-router-dom" -const inventoryLevels = inventoryItems.map((inventoryItem) => ({ - location_id: stockLocations[0].id, - stocked_quantity: 1000000, - inventory_item_id: inventoryItem.id, -})) +export async function loader({ params }: LoaderFunctionArgs) { + const { id } = params + // TODO fetch product by id -await createInventoryLevelsWorkflow(container).run({ - input: { - inventory_levels: inventoryLevels, - }, -}) + return { + id, + } +} -logger.info("Finished seeding inventory levels data.") +const CustomPage = () => { + const { id } = useLoaderData() as Awaited<ReturnType<typeof loader>> + + return ( + <div> + <Container className="divide-y p-0"> + <div className="flex items-center justify-between px-6 py-4"> + <Heading level="h2">Product ID: {id}</Heading> + </div> + </Container> + </div> + ) +} + +export default CustomPage ``` -You use Query to retrieve the stock location, to use the first location in the application, and the inventory items. +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. -Then, you generate inventory levels for each inventory item, associating it with the first stock location. +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. -Finally, you use the `createInventoryLevelsWorkflow` from Medusa's core workflows to create the inventory levels. +### When to Use Route Loaders -### Test Script +A route loader is executed before the route is loaded. So, it will block navigation until the loader function is resolved. -To test out the script, run the following command in your project's directory: +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. -```bash -npx medusa exec ./src/scripts/demo-products.ts +*** + +## Other React Router Utilities + +### Route Handles + +Route handles are available starting from Medusa v2.5.1. + +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: + +```tsx title="src/admin/routes/custom/page.tsx" +export const handle = { + sandbox: true, +} ``` -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. +### 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. + + +# Admin Widgets + +In this chapter, you’ll learn more about widgets and how to use them. + +## What is an Admin Widget? + +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. + +For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. + +*** + +## How to Create a Widget? + +### Prerequisites + +- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) + +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: + + + +```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" + +// The widget +const ProductWidget = () => { + return ( + <Container className="divide-y p-0"> + <div className="flex items-center justify-between px-6 py-4"> + <Heading level="h2">Product Widget</Heading> + </div> + </Container> + ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +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. + +The widget component must be created as an arrow function. + +### Test the Widget + +To test out the widget, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, open a product’s details page. You’ll find your custom widget at the top of the page. + +*** + +## Props Passed in Detail Pages + +Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. + +For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: + +```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" +import { + DetailWidgetProps, + AdminProduct, +} from "@medusajs/framework/types" + +// The widget +const ProductWidget = ({ + data, +}: DetailWidgetProps<AdminProduct>) => { + return ( + <Container className="divide-y p-0"> + <div className="flex items-center justify-between px-6 py-4"> + <Heading level="h2"> + Product Widget {data.title} + </Heading> + </div> + </Container> + ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +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`. + +*** + +## Injection Zone + +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. + +*** + +## 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. + + +# Handling CORS in API Routes + +In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes. + +## CORS Overview + +Cross-Origin Resource Sharing (CORS) allows only configured origins to access your API Routes. + +For example, if you allow only origins starting with `http://localhost:7001` to access your Admin API Routes, other origins accessing those routes get a CORS error. + +### CORS Configurations + +The `storeCors` and `adminCors` properties of Medusa's `http` configuration set the allowed origins for routes starting with `/store` and `/admin` respectively. + +These configurations accept a URL pattern to identify allowed origins. + +For example: + +```js title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + http: { + storeCors: "http://localhost:8000", + adminCors: "http://localhost:7001", + // ... + }, + }, +}) +``` + +This allows the `http://localhost:7001` origin to access the Admin API Routes, and the `http://localhost:8000` origin to access Store API Routes. + +Learn more about the CORS configurations in [this resource guide](https://docs.medusajs.com/resources/references/medusa-config#http/index.html.md). + +*** + +## CORS in Store and Admin Routes + +To disable the CORS middleware for a route, export a `CORS` variable in the route file with its value set to `false`. + +For example: + +```ts title="src/api/store/custom/route.ts" highlights={[["15"]]} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "[GET] Hello world!", + }) +} + +export const CORS = false +``` + +This disables the CORS middleware on API Routes at the path `/store/custom`. + +*** + +## CORS in Custom Routes + +If you create a route that doesn’t start with `/store` or `/admin`, you must apply the CORS middleware manually. Otherwise, all requests to your API route lead to a CORS error. + +You can do that in the exported middlewares configurations in `src/api/middlewares.ts`. + +For example: + +```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" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom*", + middlewares: [ + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + const configModule: ConfigModule = + req.scope.resolve("configModule") + + return cors({ + origin: parseCorsOrigins( + configModule.projectConfig.http.storeCors + ), + credentials: true, + })(req, res, next) + }, + ], + }, + ], +}) +``` + +This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`. # Pass Additional Data to Medusa's API Route @@ -6243,118 +6167,6 @@ createProductsWorkflow.hooks.productsCreated( This updates the products to their original state before adding the brand to their `metadata` property. -# Handling CORS in API Routes - -In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes. - -## CORS Overview - -Cross-Origin Resource Sharing (CORS) allows only configured origins to access your API Routes. - -For example, if you allow only origins starting with `http://localhost:7001` to access your Admin API Routes, other origins accessing those routes get a CORS error. - -### CORS Configurations - -The `storeCors` and `adminCors` properties of Medusa's `http` configuration set the allowed origins for routes starting with `/store` and `/admin` respectively. - -These configurations accept a URL pattern to identify allowed origins. - -For example: - -```js title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - http: { - storeCors: "http://localhost:8000", - adminCors: "http://localhost:7001", - // ... - }, - }, -}) -``` - -This allows the `http://localhost:7001` origin to access the Admin API Routes, and the `http://localhost:8000` origin to access Store API Routes. - -Learn more about the CORS configurations in [this resource guide](https://docs.medusajs.com/resources/references/medusa-config#http/index.html.md). - -*** - -## CORS in Store and Admin Routes - -To disable the CORS middleware for a route, export a `CORS` variable in the route file with its value set to `false`. - -For example: - -```ts title="src/api/store/custom/route.ts" highlights={[["15"]]} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "[GET] Hello world!", - }) -} - -export const CORS = false -``` - -This disables the CORS middleware on API Routes at the path `/store/custom`. - -*** - -## CORS in Custom Routes - -If you create a route that doesn’t start with `/store` or `/admin`, you must apply the CORS middleware manually. Otherwise, all requests to your API route lead to a CORS error. - -You can do that in the exported middlewares configurations in `src/api/middlewares.ts`. - -For example: - -```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" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom*", - middlewares: [ - ( - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - const configModule: ConfigModule = - req.scope.resolve("configModule") - - return cors({ - origin: parseCorsOrigins( - configModule.projectConfig.http.storeCors - ), - credentials: true, - })(req, res, next) - }, - ], - }, - ], -}) -``` - -This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`. - - # Throwing and Handling Errors 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. @@ -6676,265 +6488,105 @@ export async function POST( 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. -# Protected Routes - -In this chapter, you’ll learn how to create protected routes. - -## What is a Protected Route? +# API Route Parameters -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. +In this chapter, you’ll learn about path, query, and request body parameters. -*** +## Path Parameters -## Default Protected Routes +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]`. -Medusa applies an authentication guard on routes starting with `/admin`, including custom API routes. +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: -Requests to `/admin` must be user-authenticated to access the route. +```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -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. +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[GET] Hello ${req.params.id}!`, + }) +} +``` -*** +The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs. -## Protect Custom API Routes +### Multiple Path Parameters -To protect custom API Routes to only allow authenticated customer or admin users, use the `authenticate` middleware from the Medusa Framework. +To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`. -For example: +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: -```ts title="src/api/middlewares.ts" highlights={highlights} -import { - defineMiddlewares, - authenticate, +```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} +import type { + MedusaRequest, + MedusaResponse, } from "@medusajs/framework/http" -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom/admin*", - middlewares: [authenticate("user", ["session", "bearer", "api-key"])], - }, - { - matcher: "/custom/customer*", - middlewares: [authenticate("customer", ["session", "bearer"])], - }, - ], -}) +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[GET] Hello ${ + req.params.id + } - ${req.params.name}!`, + }) +} ``` -The `authenticate` middleware function accepts three parameters: - -1. The type of user authenticating. Use `user` for authenticating admin users, and `customer` for authenticating customers. You can also pass `*` to allow all types of users. -2. An array of 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. +You access the `id` and `name` path parameters using the `req.params` property. *** -## Authentication Opt-Out +## Query Parameters -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 can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value. For example: -```ts title="src/api/admin/custom/route.ts" highlights={[["15"]]} -import type { - AuthenticatedMedusaRequest, +```ts title="src/api/hello-world/route.ts" highlights={queryHighlights} +import type { + MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" export const GET = async ( - req: AuthenticatedMedusaRequest, + req: MedusaRequest, res: MedusaResponse ) => { res.json({ - message: "Hello", + message: `Hello ${req.query.name}`, }) } - -export const AUTHENTICATE = false ``` -Now, any request sent to the `/admin/custom` API route is allowed, regardless if the admin user is authenticated. +The value of `req.query.name` is the value passed in `?name=John`, for example. -*** +### Validate Query Parameters -## Authenticated Request Type +You can apply validation rules on received query parameters to ensure they match specified rules and types. -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`. +Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-query-paramters/index.html.md). -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`. +*** -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. +## Request Body Parameters -### Retrieve Logged-In Customer's Details +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. -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. +Learn more about configuring body parsing in [this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md). For example: -```ts title="src/api/store/custom/route.ts" highlights={[["19", "req.auth_context.actor_id", "Access the logged-in customer's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights} import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { Modules } from "@medusajs/framework/utils" -import { ICustomerModuleService } from "@medusajs/framework/types" - -export const GET = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - if (req.auth_context?.actor_id) { - // retrieve customer - const customerModuleService: ICustomerModuleService = req.scope.resolve( - Modules.CUSTOMER - ) - - const customer = await customerModuleService.retrieveCustomer( - req.auth_context.actor_id - ) - } - - // ... -} -``` - -In this example, you resolve the Customer Module's main service, then use it to retrieve the logged-in customer, if available. - -### Retrieve Logged-In Admin User's Details - -You can access the logged-in admin user’s ID in all API Routes starting with `/admin` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object. - -For example: - -```ts title="src/api/admin/custom/route.ts" highlights={[["17", "req.auth_context.actor_id", "Access the logged-in admin user's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { Modules } from "@medusajs/framework/utils" -import { IUserModuleService } from "@medusajs/framework/types" - -export const GET = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - const userModuleService: IUserModuleService = req.scope.resolve( - Modules.USER - ) - - const user = await userModuleService.retrieveUser( - req.auth_context.actor_id - ) - - // ... -} -``` - -In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user. - - -# API Route Parameters - -In this chapter, you’ll learn about path, query, and request body parameters. - -## Path Parameters - -To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`. - -For example, to create an API Route at the path `/hello-world/:id`, where `:id` is a path parameter, create the file `src/api/hello-world/[id]/route.ts` with the following content: - -```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[GET] Hello ${req.params.id}!`, - }) -} -``` - -The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs. - -### Multiple Path Parameters - -To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`. - -For example, to create an API route at `/hello-world/:id/name/:name`, create the file `src/api/hello-world/[id]/name/[name]/route.ts` with the following content: - -```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[GET] Hello ${ - req.params.id - } - ${req.params.name}!`, - }) -} -``` - -You access the `id` and `name` path parameters using the `req.params` property. - -*** - -## Query Parameters - -You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value. - -For example: - -```ts title="src/api/hello-world/route.ts" highlights={queryHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `Hello ${req.query.name}`, - }) -} -``` - -The value of `req.query.name` is the value passed in `?name=John`, for example. - -### Validate Query Parameters - -You can apply validation rules on received query 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-query-paramters/index.html.md). - -*** - -## Request Body Parameters - -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. - -Learn more about configuring body parsing in [this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md). - -For example: - -```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights} -import type { - MedusaRequest, + MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" @@ -7041,7 +6693,7 @@ export default defineMiddlewares({ The `defineMiddlewares` function accepts a middleware configurations object that has the property `routes`. `routes`'s value is an array of middleware route objects, each having the following properties: - `matcher`: a string or regular expression indicating the API route path to apply the middleware on. The regular expression must be compatible with [path-to-regexp](https://github.com/pillarjs/path-to-regexp). -- `middlewares`: An array of middleware functions. +- `middlewares`: An array of global and route middleware functions. 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`. @@ -7066,7 +6718,7 @@ Received a request! ## How to Create a Route Middleware? -In the previous section, you learned how to create a global middleware. You define the route middleware in the same way, but you specify an additional property `method`. Its value is one or more HTTP methods to apply the middleware to. +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: @@ -7276,6 +6928,8 @@ And the route middlewares are sorted into the following order before they're reg 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. + ### Middlewares and Route Execution Order When a request is sent to an API route, the global middlewares are executed first, then the route middlewares, and finally the route handler. @@ -7322,9 +6976,357 @@ The global middleware runs first, then the route middleware, and finally the rou ## Overriding Middlewares -A middleware can never override an existing middleware. Instead, middlewares are added to the end of the middleware stack. +A middleware can not override an existing middleware. Instead, middlewares are added to the end of the middleware stack. + +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. + + +# Protected Routes + +In this chapter, you’ll learn how to create protected routes. + +## What is a Protected Route? + +A protected route is a route that requires requests to be user-authenticated before performing the route's functionality. Otherwise, the request fails, and the user is prevented access. + +*** + +## Default Protected Routes + +Medusa applies an authentication guard on routes starting with `/admin`, including custom API routes. + +Requests to `/admin` must be user-authenticated to access the route. + +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. + +*** + +## Protect Custom API Routes + +To protect custom API Routes to only allow authenticated customer or admin users, use the `authenticate` middleware from the Medusa Framework. + +For example: + +```ts title="src/api/middlewares.ts" highlights={highlights} +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/admin*", + middlewares: [authenticate("user", ["session", "bearer", "api-key"])], + }, + { + matcher: "/custom/customer*", + middlewares: [authenticate("customer", ["session", "bearer"])], + }, + ], +}) +``` + +The `authenticate` middleware function accepts three parameters: + +1. The type of user authenticating. Use `user` for authenticating admin users, and `customer` for authenticating customers. You can also pass `*` to allow all types of users. +2. An array of 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. + +*** + +## Authentication Opt-Out + +To disable the authentication guard on custom routes under the `/admin` path prefix, export an `AUTHENTICATE` variable in the route file with its value set to `false`. + +For example: + +```ts title="src/api/admin/custom/route.ts" highlights={[["15"]]} +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "Hello", + }) +} + +export const AUTHENTICATE = false +``` + +Now, any request sent to the `/admin/custom` API route is allowed, regardless if the admin user is authenticated. + +*** + +## Authenticated Request Type + +To access the authentication details in an API route, such as the logged-in user's ID, set the type of the first request parameter to `AuthenticatedMedusaRequest`. It extends `MedusaRequest`. + +The `auth_context.actor_id` property of `AuthenticatedMedusaRequest` holds the ID of the authenticated user or customer. If there isn't any authenticated user or customer, `auth_context` is `undefined`. + +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. + +### Retrieve Logged-In Customer's Details + +You can access the logged-in customer’s ID in all API routes starting with `/store` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object. + +For example: + +```ts title="src/api/store/custom/route.ts" highlights={[["19", "req.auth_context.actor_id", "Access the logged-in customer's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { Modules } from "@medusajs/framework/utils" +import { ICustomerModuleService } from "@medusajs/framework/types" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + if (req.auth_context?.actor_id) { + // retrieve customer + const customerModuleService: ICustomerModuleService = req.scope.resolve( + Modules.CUSTOMER + ) + + const customer = await customerModuleService.retrieveCustomer( + req.auth_context.actor_id + ) + } + + // ... +} +``` + +In this example, you resolve the Customer Module's main service, then use it to retrieve the logged-in customer, if available. + +### Retrieve Logged-In Admin User's Details + +You can access the logged-in admin user’s ID in all API Routes starting with `/admin` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object. + +For example: + +```ts title="src/api/admin/custom/route.ts" highlights={[["17", "req.auth_context.actor_id", "Access the logged-in admin user's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { Modules } from "@medusajs/framework/utils" +import { IUserModuleService } from "@medusajs/framework/types" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const userModuleService: IUserModuleService = req.scope.resolve( + Modules.USER + ) + + const user = await userModuleService.retrieveUser( + req.auth_context.actor_id + ) + + // ... +} +``` + +In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user. + + +# Seed Data with Custom CLI Script + +In this chapter, you'll learn how to seed data using a custom CLI script. + +## How to Seed Data + +To seed dummy data for development or demo purposes, use a custom CLI script. + +In the CLI script, use your custom workflows or Medusa's existing workflows, which you can browse in [this reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md), to seed data. + +### Example: Seed Dummy Products + +In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products. + +First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script: + +```bash npm2yarn +npm install --save-dev @faker-js/faker +``` + +Then, create the file `src/scripts/demo-products.ts` with the following content: + +```ts title="src/scripts/demo-products.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" +import { ExecArgs } from "@medusajs/framework/types" +import { faker } from "@faker-js/faker" +import { + ContainerRegistrationKeys, + Modules, + ProductStatus, +} from "@medusajs/framework/utils" +import { + createInventoryLevelsWorkflow, + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +export default async function seedDummyProducts({ + container, +}: ExecArgs) { + const salesChannelModuleService = container.resolve( + Modules.SALES_CHANNEL + ) + const logger = container.resolve( + ContainerRegistrationKeys.LOGGER + ) + const query = container.resolve( + ContainerRegistrationKeys.QUERY + ) + + const defaultSalesChannel = await salesChannelModuleService + .listSalesChannels({ + name: "Default Sales Channel", + }) + + const sizeOptions = ["S", "M", "L", "XL"] + const colorOptions = ["Black", "White"] + const currency_code = "eur" + const productsNum = 50 + + // TODO seed products +} +``` + +So far, in the script, you: + +- Resolve the Sales Channel Module's main service to retrieve the application's default sales channel. This is the sales channel the dummy products will be available in. +- Resolve the Logger to log messages in the terminal, and Query to later retrieve data useful for the seeded products. +- Initialize some default data to use when seeding the products next. + +Next, replace the `TODO` with the following: + +```ts title="src/scripts/demo-products.ts" +const productsData = new Array(productsNum).fill(0).map((_, index) => { + const title = faker.commerce.product() + "_" + index + return { + title, + is_giftcard: true, + description: faker.commerce.productDescription(), + status: ProductStatus.PUBLISHED, + options: [ + { + title: "Size", + values: sizeOptions, + }, + { + title: "Color", + values: colorOptions, + }, + ], + images: [ + { + url: faker.image.urlPlaceholder({ + text: title, + }), + }, + { + url: faker.image.urlPlaceholder({ + text: title, + }), + }, + ], + variants: new Array(10).fill(0).map((_, variantIndex) => ({ + title: `${title} ${variantIndex}`, + sku: `variant-${variantIndex}${index}`, + prices: new Array(10).fill(0).map((_, priceIndex) => ({ + currency_code, + amount: 10 * priceIndex, + })), + options: { + Size: sizeOptions[Math.floor(Math.random() * 3)], + }, + })), + shipping_profile_id: "sp_123", + sales_channels: [ + { + id: defaultSalesChannel[0].id, + }, + ], + } +}) + +// TODO seed products +``` + +You generate fifty products using the sales channel and variables you initialized, and using Faker for random data, such as the product's title or images. + +Then, replace the new `TODO` with the following: + +```ts title="src/scripts/demo-products.ts" +const { result: products } = await createProductsWorkflow(container).run({ + input: { + products: productsData, + }, +}) + +logger.info(`Seeded ${products.length} products.`) + +// TODO add inventory levels +``` + +You create the generated products using the `createProductsWorkflow` imported previously from `@medusajs/medusa/core-flows`. It accepts the product data as input, and returns the created products. + +Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following: + +```ts title="src/scripts/demo-products.ts" +logger.info("Seeding inventory levels.") + +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: ["id"], +}) + +const { data: inventoryItems } = await query.graph({ + entity: "inventory_item", + fields: ["id"], +}) + +const inventoryLevels = inventoryItems.map((inventoryItem) => ({ + location_id: stockLocations[0].id, + stocked_quantity: 1000000, + inventory_item_id: inventoryItem.id, +})) + +await createInventoryLevelsWorkflow(container).run({ + input: { + inventory_levels: inventoryLevels, + }, +}) + +logger.info("Finished seeding inventory levels data.") +``` + +You use Query to retrieve the stock location, to use the first location in the application, and the inventory items. + +Then, you generate inventory levels for each inventory item, associating it with the first stock location. + +Finally, you use the `createInventoryLevelsWorkflow` from Medusa's core workflows to create the inventory levels. + +### Test Script + +To test out the script, run the following command in your project's directory: + +```bash +npx medusa exec ./src/scripts/demo-products.ts +``` -For example, if you define a custom validation middleware on an existing route, such as `validateAndTransformBody`, then both the original and the custom validation middleware will run. +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. # Request Body and Query Parameter Validation @@ -7678,20 +7680,206 @@ This API route opens a stream by setting the `Content-Type` in the header to `te 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. -# Emit Workflow and Service Events +# Event Data Payload -In this chapter, you'll learn about event types and how to emit an event in a service or workflow. +In this chapter, you'll learn how subscribers receive an event's data payload. -## Event Types +## Access Event's Data Payload -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. +When events are emitted, they’re emitted with a data payload. -There are two types of events in Medusa: +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. -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. +For example: -### Which Event Type to Use? +```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" + +export default async function productCreateHandler({ + event, +}: SubscriberArgs<{ id: string }>) { + const productId = event.data.id + console.log(`The product ${productId} was created`) +} + +export const config: SubscriberConfig = { + event: "product.created", +} +``` + +The `event` object has the following properties: + +- 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. + +This logs the product ID received in the `product.created` event’s data payload to the console. + +{/* --- + +## List of Events with Data Payload + +Refer to [this reference](!resources!/events-reference) for a full list of events emitted by Medusa and their data payloads. */} + + +# Configure Data Model Properties + +In this chapter, you’ll learn how to configure data model properties. + +## Property’s Default Value + +Use the `default` method on a property's definition to specify the default value of a property. + +For example: + +```ts highlights={defaultHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + color: model + .enum(["black", "white"]) + .default("black"), + age: model + .number() + .default(0), + // ... +}) + +export default MyCustom +``` + +In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`. + +*** + +## Nullable Property + +Use the `nullable` method to indicate that a property’s value can be `null`. + +For example: + +```ts highlights={nullableHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + price: model.bigNumber().nullable(), + // ... +}) + +export default MyCustom +``` + +*** + +## Unique Property + +The `unique` method indicates that a property’s value must be unique in the database through a unique index. + +For example: + +```ts highlights={uniqueHighlights} +import { model } from "@medusajs/framework/utils" + +const User = model.define("user", { + email: model.text().unique(), + // ... +}) + +export default User +``` + +In this example, multiple users can’t have the same email. + + +# Add Data Model Check Constraints + +In this chapter, you'll learn how to add check constraints to your data model. + +## What is a Check Constraint? + +A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown. + +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. + +*** + +## How to Set a Check Constraint? + +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 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. + +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: + +```ts highlights={checks2Highlights} +import { model } from "@medusajs/framework/utils" + +const CustomProduct = model.define("custom_product", { + // ... + price: model.bigNumber(), +}) +.checks([ + { + name: "custom_product_price_check", + expression: (columns) => `${columns.price} >= 0`, + }, +]) +``` + +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). + +*** + +## Apply in Migrations + +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: + +```bash +npx medusa db:generate custom_module +npx medusa db:migrate +``` + +The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. + + +# Emit Workflow and Service Events + +In this chapter, you'll learn about event types and how to emit an event in a service or workflow. + +## Event Types + +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: + +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. + +### 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. @@ -7846,1097 +8034,1177 @@ If you execute the `performAction` method of your service, the event is emitted Any subscribers listening to the event are also executed. -# Event Data Payload - -In this chapter, you'll learn how subscribers receive an event's data payload. - -## Access Event's Data Payload +# Data Model Default Properties -When events are emitted, they’re emitted with a data payload. +In this chapter, you'll learn about the properties available by default in your data model. -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. +When you create a data model, the following properties are created for you by Medusa: -For example: +- `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 title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" -export default async function productCreateHandler({ - event, -}: SubscriberArgs<{ id: string }>) { - const productId = event.data.id - console.log(`The product ${productId} was created`) -} +# Data Model Database Index -export const config: SubscriberConfig = { - event: "product.created", -} -``` +In this chapter, you’ll learn how to define a database index on a data model. -The `event` object has the following properties: +## Define Database Index on Property -- 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. +Use the `index` method on a property's definition to define a database index. -This logs the product ID received in the `product.created` event’s data payload to the console. +For example: -{/* --- +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" -## List of Events with Data Payload +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text().index( + "IDX_MY_CUSTOM_NAME" + ), +}) -Refer to [this reference](!resources!/events-reference) for a full list of events emitted by Medusa and their data payloads. */} +export default MyCustom +``` +The `index` method optionally accepts the name of the index as a parameter. -# Add Columns to a Link +In this example, you define an index on the `name` property. -In this chapter, you'll learn how to add custom columns to a link definition and manage them. +*** -## How to Add Custom Columns to a Link's Table? +## Define Database Index on Data Model -The `defineLink` function used to define a link accepts a third parameter, which is an object of options. +A data model has an `indexes` method that defines database indices on its properties. -To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property: +The index can be on multiple columns (composite index). For example: -```ts highlights={linkHighlights} -import HelloModule from "../modules/hello" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" +```ts highlights={dataModelIndexHighlights} +import { model } from "@medusajs/framework/utils" -export default defineLink( - ProductModule.linkable.product, - HelloModule.linkable.myCustom, +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ { - database: { - extraColumns: { - metadata: { - type: "json", - }, - }, - }, - } -) -``` - -This adds to the table created for the link between `product` and `myCustom` a `metadata` column of type `json`. + on: ["name", "age"], + }, +]) -### Database Options +export default MyCustom +``` -The `database` property defines configuration for the table created in the database. +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. -Its `extraColumns` property defines custom columns to create in the link's table. +In the above example, you define a composite index on the `name` and `age` properties. -`extraColumns`'s value is an object whose keys are the names of the columns, and values are the column's configurations as an object. +### Index Conditions -### Column Configurations +An index can have conditions. For example: -The column's configurations object accepts the following properties: +```ts highlights={conditionHighlights} +import { model } from "@medusajs/framework/utils" -- `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. +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + where: { + age: 30, + }, + }, +]) -*** +export default MyCustom +``` -## Set Custom Column when Creating Link +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. -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. +In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`. -For example: +A property's condition can be a negation. 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 highlights={negationHighlights} +import { model } from "@medusajs/framework/utils" -```ts -await link.create({ - [Modules.PRODUCT]: { - product_id: "123", - }, - HELLO_MODULE: { - my_custom_id: "321", - }, - data: { - metadata: { - test: true, +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number().nullable(), +}).indexes([ + { + on: ["name", "age"], + where: { + age: { + $ne: null, + }, }, }, -}) +]) + +export default MyCustom ``` -*** +A property's value in `where` can be an object having a `$ne` property. `$ne`'s value indicates what the specified property's value shouldn't be. -## Retrieve Custom Column with Link +In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`. -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. +### Unique Database Index -For example: +The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index. -```ts highlights={retrieveHighlights} -import productHelloLink from "../links/product-hello" +For example: -// ... +```ts highlights={uniqueHighlights} +import { model } from "@medusajs/framework/utils" -const { data } = await query.graph({ - entity: productHelloLink.entryPoint, - fields: ["metadata", "product.*", "my_custom.*"], - filters: { - product_id: "prod_123", +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + unique: true, }, -}) +]) + +export default MyCustom ``` -This retrieves the product of id `prod_123` and its linked `my_custom` records. +This creates a unique composite index on the `name` and `age` properties. -In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link. -*** +# Infer Type of Data Model -## Update Custom Column's Value +In this chapter, you'll learn how to infer the type of a data model. -Link's `create` method updates a link's data if the link between the specified records already exists. +## How to Infer Type of Data Model? -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. +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. For example: ```ts -await link.create({ - [Modules.PRODUCT]: { - product_id: "123", - }, - HELLO_MODULE: { - my_custom_id: "321", - }, - data: { - metadata: { - test: false, - }, - }, -}) -``` +import { InferTypeOf } from "@medusajs/framework/types" +import { MyCustom } from "../models/my-custom" // relative path to the model +export type MyCustom = InferTypeOf<typeof MyCustom> +``` -# Module Link Direction +`InferTypeOf` accepts as a type argument the type of the data model. -In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case. +Since the `MyCustom` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. -## Link Direction +You can now use the `MyCustom` type to reference a data model in other types, such as in workflow inputs or service method outputs: -The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`. +```ts title="Example Service" +// other imports... +import { InferTypeOf } from "@medusajs/framework/types" +import { MyCustom } from "../models/my-custom" -For example, the following defines a link from the `helloModuleService`'s `myCustom` data model to the Product Module's `product` data model: +type MyCustom = InferTypeOf<typeof MyCustom> -```ts -export default defineLink( - HelloModule.linkable.myCustom, - ProductModule.linkable.product -) +class HelloModuleService extends MedusaService({ MyCustom }) { + async doSomething(): Promise<MyCustom> { + // ... + } +} ``` -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 -) -``` +# Manage Relationships -The above links are two different links that serve different purposes. +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 -## Which Link Direction to Use? +### BelongsTo Side of One-to-One -### Extend Data Models +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. -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, 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: -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 highlights={belongsHighlights} +// when creating an email +const email = await helloModuleService.createEmails({ + // other properties... + user_id: "123", +}) -```ts -export default defineLink( - ProductModule.linkable.product, - HelloModule.linkable.subtitle -) +// when updating an email +const email = await helloModuleService.updateEmails({ + id: "321", + // other properties... + user_id: "123", +}) ``` -### Associate Data Models - -If you're linking data models to indicate an association between them, define the link from the custom data model to the main data model. - -For example, 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`: +In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to. -```ts -export default defineLink( - HelloModule.linkable.post, - ProductModule.linkable.product -) -``` +### HasOne Side +When you create a record of a data model that has one of another, pass the ID of the other data model's record in the relation property. -# Link +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: -In this chapter, you’ll learn what Link is and how to use it to manage links. +```ts highlights={hasOneHighlights} +// when creating a user +const user = await helloModuleService.createUsers({ + // other properties... + email: "123", +}) -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. +// when updating a user +const user = await helloModuleService.updateUsers({ + id: "321", + // other properties... + email: "123", +}) +``` -## What is Link? +In the example above, you pass the `email` property when creating or updating a user to specify the email it has. -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: +## Manage One-to-Many Relationship -```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. -export async function POST( - req: MedusaRequest, - res: MedusaResponse -): Promise<void> { - const link = req.scope.resolve( - ContainerRegistrationKeys.LINK - ) - - // ... -} +When you create a record of the data model on the `belongsTo` side, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property. + +For example, assuming you have the [Product and Store data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-many-relationship/index.html.md), set a product's store ID as follows: + +```ts highlights={manyBelongsHighlights} +// when creating a product +const product = await helloModuleService.createProducts({ + // other properties... + store_id: "123", +}) + +// when updating a product +const product = await helloModuleService.updateProducts({ + id: "321", + // other properties... + store_id: "123", +}) ``` -You can use its methods to manage links, such as create or delete links. +In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to. *** -## Create Link +## Manage Many-to-Many Relationship -To create a link between records of two data models, use the `create` method of Link. +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. -For example: +### Create Associations -```ts -import { Modules } from "@medusajs/framework/utils" +When you create a record of a data model that has a many-to-many relationship to another data model, pass an array of IDs of the other data model's records in the relation property. -// ... +For example, assuming you have the [Order and Product data models from the previous chapter](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: -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_123", - }, +```ts highlights={manyHighlights} +// when creating a product +const product = await helloModuleService.createProducts({ + // other properties... + orders: ["123", "321"], }) -``` -The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules. +// when creating an order +const order = await helloModuleService.createOrders({ + id: "321", + // other properties... + products: ["123", "321"], +}) +``` -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. +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 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. +### Update Associations -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. +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. -## Dismiss Link +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: -To remove a link between records of two data models, use the `dismiss` method of Link. +```ts +const product = await helloModuleService.updateProducts({ + id: "123", + // other properties... + orders: ["321"], +}) +``` -For example: +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. -```ts -import { Modules } from "@medusajs/framework/utils" +So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product: -// ... +```ts highlights={updateAssociationHighlights} +const product = await helloModuleService.retrieveProduct( + "123", + { + relations: ["orders"], + } +) -await link.dismiss({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_123", - }, +const updatedProduct = await helloModuleService.updateProducts({ + id: product.id, + // other properties... + orders: [ + ...product.orders.map((order) => order.id), + "321", + ], }) ``` -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. +This keeps existing associations between the product and orders, and adds a new one. *** -## Cascade Delete Linked Records +## Manage Many-to-Many Relationship with pivotEntity -If a record is deleted, use the `delete` method of Link to delete all linked records. +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. -For example: +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. -```ts -import { Modules } from "@medusajs/framework/utils" +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: -// ... +```ts highlights={["4"]} +class HelloModuleService extends MedusaService({ + Order, + Product, + OrderProduct, +}) {} +``` -await productModuleService.deleteVariants([variant.id]) +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. -await link.delete({ - [Modules.PRODUCT]: { - product_id: "prod_123", +For example: + +```ts +// create order-product association +const orderProduct = await helloModuleService.createOrderProducts({ + order_id: "123", + product_id: "123", + metadata: { + test: true, + }, +}) + +// update order-product association +const orderProduct = await helloModuleService.updateOrderProducts({ + id: "123", + metadata: { + test: false, }, }) + +// delete order-product association +await helloModuleService.deleteOrderProducts("123") ``` -This deletes all records linked to the deleted product. +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. *** -## Restore Linked Records +## Retrieve Records of Relation -If a record that was previously soft-deleted is now restored, use the `restore` method of Link to restore all linked records. +The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options. -For example: +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 -import { Modules } from "@medusajs/framework/utils" +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"], + } +) +``` -await productModuleService.restoreProducts(["prod_123"]) +In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. -await link.restore({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, -}) -``` +# Data Model’s Primary Key -# Query +In this chapter, you’ll learn how to configure the primary key of a data model. -In this chapter, you’ll learn about Query and how to use it to fetch data from modules. +## primaryKey Method -## What is Query? +To set any `id`, `text`, or `number` property as a primary key, use the `primaryKey` method. -Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key. +For example: -In your resources, such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s commerce modules. +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" -*** +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + // ... +}) -## Query Example +export default MyCustom +``` -For example, create the route `src/api/query/route.ts` with the following content: +In the example above, the `id` property is defined as the data model's primary key. -```ts title="src/api/query/route.ts" highlights={exampleHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) +# Data Model Property Types - const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], - }) +In this chapter, you’ll learn about the types of properties in a data model’s schema. - res.json({ my_customs: myCustoms }) -} -``` +## id -In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key. +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 run a query using its `graph` method. This method accepts as a parameter an object with the following required properties: +For example: -- `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. +```ts highlights={idHighlights} +import { model } from "@medusajs/framework/utils" -The method returns an object that has a `data` property, which holds an array of the retrieved data. For example: +const MyCustom = model.define("my_custom", { + id: model.id(), + // ... +}) -```json title="Returned Data" -{ - "data": [ - { - "id": "123", - "name": "test" - } - ] -} +export default MyCustom ``` *** -## Querying the Graph +## text -When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates. +The `text` method defines a string property. -This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them. +For example: + +```ts highlights={textHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + name: model.text(), + // ... +}) + +export default MyCustom +``` *** -## Retrieve Linked Records +## number -Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`. +The `number` method defines a number property. For example: -```ts highlights={[["6"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: [ - "id", - "name", - "product.*", - ], +```ts highlights={numberHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + age: model.number(), + // ... }) + +export default MyCustom ``` -`.*` 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 +## float -If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`. +This property is only available after [Medusa v2.1.2](https://github.com/medusajs/medusa/releases/tag/v2.1.2). + +The `float` method defines a number property that allows for values with decimal places. + +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). For example: -```ts highlights={[["6"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: [ - "id", - "name", - "products.*", - ], +```ts highlights={floatHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + rating: model.float(), + // ... }) + +export default MyCustom ``` -### 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. +## bigNumber -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. +The `bigNumber` method defines a number property that expects large numbers, such as prices. -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. +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). For example: -```ts highlights={queryLinkTableHighlights} -import productCustomLink from "../../../links/product-custom" - -// ... +```ts highlights={bigNumberHighlights} +import { model } from "@medusajs/framework/utils" -const { data: productCustoms } = await query.graph({ - entity: productCustomLink.entryPoint, - fields: ["*", "product.*", "my_custom.*"], - pagination: { - take: 5, - skip: 0, - }, +const MyCustom = model.define("my_custom", { + price: model.bigNumber(), + // ... }) + +export default MyCustom ``` -In the object passed to the `graph` method: +*** -- 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. +## boolean -You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination). +The `boolean` method defines a boolean property. -The returned `data` is similar to the following: +For example: -```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... - } -}] +```ts highlights={booleanHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + hasAccount: model.boolean(), + // ... +}) + +export default MyCustom ``` *** -## Apply Filters +### enum -```ts highlights={[["6"], ["7"], ["8"], ["9"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], - filters: { - id: [ - "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX", - "mc_01HWSVWK3KYHKQEE6QGS2JC3FX", - ], - }, +The `enum` method defines a property whose value can only be one of the specified values. + +For example: + +```ts highlights={enumHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + color: model.enum(["black", "white"]), + // ... }) + +export default MyCustom ``` -The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records. +The `enum` method accepts an array of possible string values. -In the example above, you filter the `my_custom` records by multiple IDs. +*** -Filters don't apply on fields of linked data models from other modules. +## dateTime -*** +The `dateTime` method defines a timestamp property. -## Apply Pagination +For example: -```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, - }, +```ts highlights={dateTimeHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + date_of_birth: model.dateTime(), + // ... }) -``` -The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records. +export default MyCustom +``` -To paginate the returned records, pass the following properties to `pagination`: +*** -- `skip`: (required to apply pagination) The number of records to skip before fetching the results. -- `take`: The number of records to fetch. +## json -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: +The `json` method defines a property whose value is a stringified JSON object. -- skip: (\`number\`) The number of records skipped. -- take: (\`number\`) The number of records requested to fetch. -- count: (\`number\`) The total number of records. +For example: -### Sort Records +```ts highlights={jsonHighlights} +import { model } from "@medusajs/framework/utils" -```ts highlights={[["5"], ["6"], ["7"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], - pagination: { - order: { - name: "DESC", - }, - }, +const MyCustom = model.define("my_custom", { + metadata: model.json(), + // ... }) + +export default MyCustom ``` -Sorting doesn't work on fields of linked data models from other modules. +*** -To sort returned records, pass an `order` property to `pagination`. +## array -The `order` property is an object whose keys are property names, and values are either: +The `array` method defines an array of strings property. -- `ASC` to sort records by that property in ascending order. -- `DESC` to sort records by that property in descending order. +For example: + +```ts highlights={arrHightlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + names: model.array(), + // ... +}) + +export default MyCustom +``` *** -## Request Query Configurations +## Properties Reference -For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that: +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. -- 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. -Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request. +# Write Migration -### Step 1: Add Middleware +In this chapter, you'll learn how to create a migration and write it manually. -The first step is to use the `validateAndTransformQuery` middleware on the `GET` route. You add the middleware in `src/api/middlewares.ts`: +## What is a Migration? -```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() - -export default defineMiddlewares({ - routes: [ - { - matcher: "/customs", - method: "GET", - middlewares: [ - validateAndTransformQuery( - GetCustomSchema, - { - defaults: [ - "id", - "name", - "products.*", - ], - isList: true, - } - ), - ], - }, - ], -}) -``` +A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It has two methods: -The `validateAndTransformQuery` accepts two parameters: +- The `up` method reflects changes on the database. +- The `down` method reverts the changes made in the `up` method. -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`. +*** -### Step 2: Use Configurations in API Route +## How to Write a Migration? -After applying this middleware, your API route now accepts the `fields`, `offset`, `limit`, and `order` query parameters mentioned above. +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 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. +Alternatively, you can manually create a migration file under the `migrations` directory of your module. -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. +For example: -For example, Create the file `src/api/customs/route.ts` with the following content: +```ts title="src/modules/hello/migrations/Migration20240429.ts" +import { Migration } from "@mikro-orm/migrations" -```ts title="src/api/customs/route.ts" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +export class Migration20240702105919 extends Migration { -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + async up(): Promise<void> { + this.addSql("create table if not exists \"my_custom\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"my_custom_pkey\" primary key (\"id\"));") + } - const { data: myCustoms } = await query.graph({ - entity: "my_custom", - ...req.queryConfig, - }) + async down(): Promise<void> { + this.addSql("drop table if exists \"my_custom\" cascade;") + } - res.json({ my_customs: myCustoms }) } ``` -This adds a `GET` API route at `/customs`, which is the API route you added the middleware for. +The migration's file name should be of the format `Migration{YEAR}{MONTH}{DAY}.ts`. The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. -In the 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 the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax. -### Test it Out +In the example above, the `up` method creates the table `my_custom`, and the `down` method drops the table if the migration is reverted. -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. +Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. -```json title="Returned Data" -{ - "my_customs": [ - { - "id": "123", - "name": "test" - } - ] -} -``` +*** -Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result. +## Run the Migration -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. +To run your migration, run the following command: +This command also syncs module links. If you don't want that, use the `--skip-links` option. -# Query Context +```bash +npx medusa db:migrate +``` -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). +This reflects the changes in the database as implemented in the migration's `up` method. -## What is Query Context? +*** -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. +## Rollback the Migration -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). +To rollback or revert the last migration you ran for a module, run the following command: + +```bash +npx medusa db:rollback helloModuleService +``` + +This rolls back the last ran migration on the Hello Module. *** -## How to Use Query Context +## More Database Commands -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`). +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). -You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument. -For example, to retrieve posts using Query while passing the user's language as a context: +# Searchable Data Model Property -```ts -const { data } = await query.graph({ - entity: "post", - fields: ["*"], - context: QueryContext({ - lang: "es", - }), -}) -``` +In this chapter, you'll learn what a searchable property is and how to define it. -In this example, you pass in the context a `lang` property whose value is `es`. +## What is a Searchable Property? -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. +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. -For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context: +When the `q` filter is passed, the data model's searchable properties are queried to find matching records. -```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 BlogModuleService extends MedusaService({ - Post, - Author, -}){ - // @ts-ignore - async listPosts( - filters?: any, - config?: FindConfig<any> | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context +## Define a Searchable Property - let posts = await super.listPosts(filters, config, sharedContext) +Use the `searchable` method on a `text` property to indicate that it's searchable. - if (context.lang === "es") { - posts = posts.map((post) => { - return { - ...post, - title: post.title + " en español", - } - }) - } +For example: - return posts - } -} +```ts highlights={searchableHighlights} +import { model } from "@medusajs/framework/utils" -export default BlogModuleService -``` +const MyCustom = model.define("my_custom", { + name: model.text().searchable(), + // ... +}) -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. +export default MyCustom +``` -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. +In this example, the `name` property is searchable. -All posts returned will now have their titles appended with "en español". +### Search Example -Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md). +If you pass a `q` filter to the `listMyCustoms` method: -### Using Pagination with Query +```ts +const myCustoms = await helloModuleService.listMyCustoms({ + q: "John", +}) +``` -If you pass pagination fields to `query.graph`, you must also override the `listAndCount` method in the service. +This retrieves records that include `John` in their `name` property. -For example, following along with the previous example, you must override the `listAndCountPosts` method of the Blog Module's service: -```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" +# Data Model Relationships -class BlogModuleService extends MedusaService({ - Post, - Author, -}){ - // @ts-ignore - async listAndCountPosts( - filters?: any, - config?: FindConfig<any> | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context +In this chapter, you’ll learn how to define relationships between data models in your module. - const result = await super.listAndCountPosts( - filters, - config, - sharedContext - ) +## What is a Relationship Property? - if (context.lang === "es") { - result.posts = posts.map((post) => { - return { - ...post, - title: post.title + " en español", - } - }) - } +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`. - return result - } -} +When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type. -export default BlogModuleService -``` +You want to create a relation between data models in the same module. -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`. +You want to create a relationship between data models in different modules. Use module links instead. *** -## Passing Query Context to Related Data Models +## One-to-One Relationship -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. +A one-to-one relationship indicates that one record of a data model belongs to or is associated with another. -For linked data models, check out the [next section](#passing-query-context-to-linked-data-models). +To define a one-to-one relationship, create relationship properties in the data models using the following methods: -For example, to pass a context for the post's authors: +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. -```ts highlights={highlights3} -const { data } = await query.graph({ - entity: "post", - fields: ["*"], - context: QueryContext({ - lang: "es", - author: QueryContext({ - lang: "es", - }), - }), -}) -``` +For example: -Then, in the `listPosts` method, you can handle the context for the post's authors: +```ts highlights={oneToOneHighlights} +import { model } from "@medusajs/framework/utils" -```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 User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email), +}) -class BlogModuleService extends MedusaService({ - Post, - Author, -}){ - // @ts-ignore - async listPosts( - filters?: any, - config?: FindConfig<any> | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context +const Email = model.define("email", { + id: model.id().primaryKey(), + user: model.belongsTo(() => User, { + mappedBy: "email", + }), +}) +``` - let posts = await super.listPosts(filters, config, sharedContext) +In the example above, a user has one email, and an email belongs to one user. - const isPostLangEs = context.lang === "es" - const isAuthorLangEs = context.author?.lang === "es" +The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model. - 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, - }, - } - }) - } +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. - return posts - } -} +### Optional Relationship -export default BlogModuleService -``` +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). -The context in `filters` will also have the context for `author`, which you can use to make transformations to the post's authors. +### One-sided One-to-One Relationship -*** +If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. -## Passing Query Context to Linked Data Models +For example: -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. +```ts highlights={oneToOneUndefinedHighlights} +import { model } from "@medusajs/framework/utils" -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: +const User = model.define("user", { + id: model.id().primaryKey(), +}) -```ts highlights={highlights5} -const { data } = await query.graph({ - entity: "product", - fields: ["*", "post.*"], - context: { - post: QueryContext({ - lang: "es", - }), - }, +const Email = model.define("email", { + id: model.id().primaryKey(), + user: model.belongsTo(() => User, { + mappedBy: undefined, + }), }) ``` -In this example, you retrieve products and their associated posts. You also pass a context for `post`, indicating the customer's language. +### One-to-One Relationship in the Database -To handle the context, you override the generated `listPosts` method of the Blog Module as explained [previously](#how-to-use-query-context). +When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property: +1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column. +2. A foreign key on the `{relation_name}_id` column to the table of the related data model. -# Create a Plugin + -In this chapter, you'll learn how to create a Medusa plugin and publish it. +*** -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. +## One-to-Many Relationship -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). +A one-to-many relationship indicates that one record of a data model has many records of another data model. -## 1. Create a Plugin Project +To define a one-to-many relationship, create relationship properties in the data models using the following methods: -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. +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. -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: +For example: -```bash -npx create-medusa-app my-plugin --plugin +```ts highlights={oneToManyHighlights} +import { model } from "@medusajs/framework/utils" + +const Store = model.define("store", { + id: model.id().primaryKey(), + products: model.hasMany(() => Product), +}) + +const Product = model.define("product", { + id: model.id().primaryKey(), + store: model.belongsTo(() => Store, { + mappedBy: "products", + }), +}) ``` -This will create a new Medusa plugin project in the `my-plugin` directory. +In this example, a store has many products, but a product belongs to one store. -### Plugin Directory Structure +### Optional Relationship -After the installation is done, the plugin structure will look like this: +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). - +### One-to-Many Relationship in the Database -- `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. +When you generate the migrations of data models that have a one-to-many relationship, the migration adds to the table of the data model that has the `belongsTo` property: + +1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `product` table will have a `store_id` column. +2. A foreign key on the `{relation_name}_id` column to the table of the related data model. + + *** -## 2. Prepare Plugin +## Many-to-Many Relationship -### Package Name +A many-to-many relationship indicates that many records of a data model can be associated with many records of another data model. -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. +To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method. For example: -```json title="package.json" -{ - "name": "@myorg/plugin-name", - // ... -} -``` - -### Package Keywords +```ts highlights={manyToManyHighlights} +import { model } from "@medusajs/framework/utils" -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: +const Order = model.define("order", { + id: model.id().primaryKey(), + products: model.manyToMany(() => Product, { + mappedBy: "orders", + pivotTable: "order_product", + joinColumn: "order_id", + inverseJoinColumn: "product_id", + }), +}) -```json title="package.json" -{ - "keywords": [ - "medusa-plugin", - "medusa-v2" - ], - // ... -} +const Product = model.define("product", { + id: model.id().primaryKey(), + orders: model.manyToMany(() => Order, { + mappedBy: "products", + }), +}) ``` -### Package Dependencies +The `manyToMany` method accepts two parameters: -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. +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. -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. +The `pivotTable`, `joinColumn`, and `inverseJoinColumn` properties are only available after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). -For example, assuming `2.5.0` is the latest Medusa version: +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. -```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", - } -} -``` +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 -## 3. Publish Plugin Locally for Development and Testing +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`. -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 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. -### Publish and Install Local Package +The pivot table has columns with foreign keys pointing to the primary key of the associated tables. The column's name is either: -### Prerequisites +- 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`. -- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) + -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. +### Many-To-Many with Custom Columns -To publish the plugin to the local registry, run the following command in your plugin project: +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. -```bash title="Plugin project" -npx medusa plugin:publish +For example: + +```ts highlights={manyToManyColumnHighlights} +import { model } from "@medusajs/framework/utils" + +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), +}) + +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(), +}) +``` + +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: + +- `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. + +*** + +## 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. + +As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. + +For example: + +```ts highlights={relationNameHighlights} +import { model } from "@medusajs/framework/utils" + +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email, { + mappedBy: "owner", + }), +}) + +const Email = model.define("email", { + id: model.id().primaryKey(), + owner: model.belongsTo(() => User, { + mappedBy: "email", + }), +}) +``` + +In this example, you specify in the `User` data model’s relationship property that the name of the relationship in the `Email` data model is `owner`. + +*** + +## Cascades + +When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it. + +For example, if a store is deleted, its products should also be deleted. + +The `cascades` method used on a data model configures which child records an operation is cascaded to. + +For example: + +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" + +const Store = model.define("store", { + id: model.id().primaryKey(), + products: model.hasMany(() => Product), +}) +.cascades({ + delete: ["products"], +}) + +const Product = model.define("product", { + id: model.id().primaryKey(), + store: model.belongsTo(() => Store, { + mappedBy: "products", + }), +}) +``` + +The `cascades` method accepts an object. Its key is the operation’s name, such as `delete`. The value is an array of relationship property names that the operation is cascaded to. + +In the example above, when a store is deleted, its associated products are also deleted. + + +# Create a Plugin + +In this chapter, you'll learn how to create a Medusa plugin and publish it. + +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. + +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + +## 1. Create a Plugin Project + +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. + +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: + +```bash +npx create-medusa-app my-plugin --plugin +``` + +This will create a new Medusa plugin project in the `my-plugin` directory. + +### Plugin Directory Structure + +After the installation is done, the plugin structure will look like this: + + + +- `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. + +*** + +## 2. Prepare Plugin + +### Package Name + +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: + +```json title="package.json" +{ + "name": "@myorg/plugin-name", + // ... +} +``` + +### Package Keywords + +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" + ], + // ... +} +``` + +### Package Dependencies + +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. + +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. + +For example, assuming `2.5.0` is the latest Medusa version: + +```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", + } +} +``` + +*** + +## 3. Publish Plugin Locally for Development and Testing + +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. + +### Publish and Install Local Package + +### Prerequisites + +- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) + +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. + +To publish the plugin to the local registry, run the following command in your plugin project: + +```bash title="Plugin project" +npx medusa plugin:publish ``` 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`. @@ -9195,1336 +9463,1491 @@ npm publish This will publish an updated version of your plugin under a new version. -# Add Data Model Check Constraints - -In this chapter, you'll learn how to add check constraints to your data model. - -## What is a Check Constraint? - -A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown. - -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. - -*** - -## How to Set a Check Constraint? - -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" +# Add Columns to a Link -const CustomProduct = model.define("custom_product", { - // ... - price: model.bigNumber(), -}) -.checks([ - (columns) => `${columns.price} >= 0`, -]) -``` +In this chapter, you'll learn how to add custom columns to a link definition and manage them. -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. +## How to Add Custom Columns to a Link's Table? -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 `defineLink` function used to define a link accepts a third parameter, which is an object of options. -You can also pass an object to the `checks` method: +To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property: -```ts highlights={checks2Highlights} -import { model } from "@medusajs/framework/utils" +```ts highlights={linkHighlights} +import HelloModule from "../modules/hello" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" -const CustomProduct = model.define("custom_product", { - // ... - price: model.bigNumber(), -}) -.checks([ +export default defineLink( + ProductModule.linkable.product, + HelloModule.linkable.myCustom, { - name: "custom_product_price_check", - expression: (columns) => `${columns.price} >= 0`, - }, -]) + database: { + extraColumns: { + metadata: { + type: "json", + }, + }, + }, + } +) ``` -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). - -*** +This adds to the table created for the link between `product` and `myCustom` a `metadata` column of type `json`. -## Apply in Migrations +### Database Options -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. +The `database` property defines configuration for the table created in the database. -To generate a migration for the data model's module then reflect it on the database, run the following command: +Its `extraColumns` property defines custom columns to create in the link's table. -```bash -npx medusa db:generate custom_module -npx medusa db:migrate -``` +`extraColumns`'s value is an object whose keys are the names of the columns, and values are the column's configurations as an object. -The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. +### Column Configurations +The column's configurations object accepts the following properties: -# Configure Data Model 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. -In this chapter, you’ll learn how to configure data model properties. +*** -## Property’s Default Value +## Set Custom Column when Creating Link -Use the `default` method on a property's definition to specify the default value of a property. +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={defaultHighlights} -import { model } from "@medusajs/framework/utils" +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 MyCustom = model.define("my_custom", { - color: model - .enum(["black", "white"]) - .default("black"), - age: model - .number() - .default(0), - // ... +```ts +await link.create({ + [Modules.PRODUCT]: { + product_id: "123", + }, + HELLO_MODULE: { + my_custom_id: "321", + }, + data: { + metadata: { + test: true, + }, + }, }) - -export default MyCustom ``` -In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`. - *** -## Nullable Property +## Retrieve Custom Column with Link -Use the `nullable` method to indicate that a property’s value can be `null`. +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 highlights={nullableHighlights} -import { model } from "@medusajs/framework/utils" +```ts highlights={retrieveHighlights} +import productHelloLink from "../links/product-hello" -const MyCustom = model.define("my_custom", { - price: model.bigNumber().nullable(), - // ... -}) +// ... -export default MyCustom +const { data } = await query.graph({ + entity: productHelloLink.entryPoint, + fields: ["metadata", "product.*", "my_custom.*"], + filters: { + product_id: "prod_123", + }, +}) ``` +This retrieves the product of id `prod_123` and its linked `my_custom` records. + +In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link. + *** -## Unique Property +## Update Custom Column's Value -The `unique` method indicates that a property’s value must be unique in the database through a unique index. +Link's `create` method updates a link's data if the link between the specified records already exists. -For example: +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. -```ts highlights={uniqueHighlights} -import { model } from "@medusajs/framework/utils" +For example: -const User = model.define("user", { - email: model.text().unique(), - // ... +```ts +await link.create({ + [Modules.PRODUCT]: { + product_id: "123", + }, + HELLO_MODULE: { + my_custom_id: "321", + }, + data: { + metadata: { + test: false, + }, + }, }) - -export default User ``` -In this example, multiple users can’t have the same email. +# Module Link Direction -# Infer Type of Data Model +In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case. -In this chapter, you'll learn how to infer the type of a data model. +## Link Direction -## How to Infer Type of Data Model? +The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`. -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. +For example, the following defines a link from the `helloModuleService`'s `myCustom` data model to the Product Module's `product` data model: -Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. +```ts +export default defineLink( + HelloModule.linkable.myCustom, + ProductModule.linkable.product +) +``` -For example: +Whereas the following defines a link from the Product Module's `product` data model to the `helloModuleService`'s `myCustom` data model: ```ts -import { InferTypeOf } from "@medusajs/framework/types" -import { MyCustom } from "../models/my-custom" // relative path to the model - -export type MyCustom = InferTypeOf<typeof MyCustom> +export default defineLink( + ProductModule.linkable.product, + HelloModule.linkable.myCustom +) ``` -`InferTypeOf` accepts as a type argument the type of the data model. +The above links are two different links that serve different purposes. -Since the `MyCustom` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. +*** -You can now use the `MyCustom` type to reference a data model in other types, such as in workflow inputs or service method outputs: +## Which Link Direction to Use? -```ts title="Example Service" -// other imports... -import { InferTypeOf } from "@medusajs/framework/types" -import { MyCustom } from "../models/my-custom" +### Extend Data Models -type MyCustom = InferTypeOf<typeof MyCustom> +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. -class HelloModuleService extends MedusaService({ MyCustom }) { - async doSomething(): Promise<MyCustom> { - // ... - } -} +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 +) ``` +### Associate Data Models -# Data Model Default Properties +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. -In this chapter, you'll learn about the properties available by default in your 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`: -When you create a data model, the following properties are created for you by Medusa: +```ts +export default defineLink( + HelloModule.linkable.post, + ProductModule.linkable.product +) +``` -- `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. +# Link -# Data Model Database Index +In this chapter, you’ll learn what Link is and how to use it to manage links. -In this chapter, you’ll learn how to define a database index on a data model. +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. -## Define Database Index on Property +## What is Link? -Use the `index` method on a property's definition to define a database index. +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 highlights={highlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text().index( - "IDX_MY_CUSTOM_NAME" - ), -}) +```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -export default MyCustom +export async function POST( + req: MedusaRequest, + res: MedusaResponse +): Promise<void> { + const link = req.scope.resolve( + ContainerRegistrationKeys.LINK + ) + + // ... +} ``` -The `index` method optionally accepts the name of the index as a parameter. - -In this example, you define an index on the `name` property. +You can use its methods to manage links, such as create or delete links. *** -## Define Database Index on Data Model +## Create Link -A data model has an `indexes` method that defines database indices on its properties. +To create a link between records of two data models, use the `create` method of Link. -The index can be on multiple columns (composite index). For example: +For example: -```ts highlights={dataModelIndexHighlights} -import { model } from "@medusajs/framework/utils" +```ts +import { Modules } 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 +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + "helloModuleService": { + my_custom_id: "mc_123", + }, +}) ``` -The `indexes` method receives an array of indices as a parameter. Each index is an object with a required `on` property indicating the properties to apply the index on. - -In the above example, you define a composite index on the `name` and `age` properties. +The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules. -### Index Conditions +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. -An index can have conditions. For example: +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. -```ts highlights={conditionHighlights} -import { model } from "@medusajs/framework/utils" +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. -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - where: { - age: 30, - }, - }, -]) +*** -export default MyCustom -``` +## Dismiss Link -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. +To remove a link between records of two data models, use the `dismiss` method of Link. -In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`. +For example: -A property's condition can be a negation. For example: +```ts +import { Modules } from "@medusajs/framework/utils" -```ts highlights={negationHighlights} -import { model } from "@medusajs/framework/utils" +// ... -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number().nullable(), -}).indexes([ - { - on: ["name", "age"], - where: { - age: { - $ne: null, - }, - }, +await link.dismiss({ + [Modules.PRODUCT]: { + product_id: "prod_123", }, -]) - -export default MyCustom + "helloModuleService": { + my_custom_id: "mc_123", + }, +}) ``` -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. +The `dismiss` method accepts the same parameter type as the [create method](#create-link). -In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`. +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. -### Unique Database Index +*** -The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index. +## Cascade Delete Linked Records + +If a record is deleted, use the `delete` method of Link to delete all linked records. For example: -```ts highlights={uniqueHighlights} -import { model } from "@medusajs/framework/utils" +```ts +import { Modules } from "@medusajs/framework/utils" -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - unique: true, - }, -]) +// ... -export default MyCustom -``` +await productModuleService.deleteVariants([variant.id]) -This creates a unique composite index on the `name` and `age` properties. +await link.delete({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, +}) +``` +This deletes all records linked to the deleted product. -# Manage Relationships +*** -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. +## Restore Linked Records -## Manage One-to-One Relationship +If a record that was previously soft-deleted is now restored, use the `restore` method of Link to restore all linked records. -### BelongsTo Side of One-to-One +For example: -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. +```ts +import { Modules } 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 an email's user ID as follows: +// ... -```ts highlights={belongsHighlights} -// when creating an email -const email = await helloModuleService.createEmails({ - // other properties... - user_id: "123", -}) +await productModuleService.restoreProducts(["prod_123"]) -// when updating an email -const email = await helloModuleService.updateEmails({ - id: "321", - // other properties... - user_id: "123", +await link.restore({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, }) ``` -In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to. - -### HasOne Side -When you create a record of a data model that has one of another, pass the ID of the other data model's record in the relation property. +# Query -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: +In this chapter, you’ll learn about Query and how to use it to fetch data from modules. -```ts highlights={hasOneHighlights} -// when creating a user -const user = await helloModuleService.createUsers({ - // other properties... - email: "123", -}) +## What is Query? -// when updating a user -const user = await helloModuleService.updateUsers({ - id: "321", - // other properties... - email: "123", -}) -``` +Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key. -In the example above, you pass the `email` property when creating or updating a user to specify the email it has. +In your resources, such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s commerce modules. *** -## Manage One-to-Many Relationship +## Query Example -In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. +For example, create the route `src/api/query/route.ts` with the following content: -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. +```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" -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: +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) -```ts highlights={manyBelongsHighlights} -// when creating a product -const product = await helloModuleService.createProducts({ - // other properties... - store_id: "123", -}) + const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + }) -// when updating a product -const product = await helloModuleService.updateProducts({ - id: "321", - // other properties... - store_id: "123", -}) + res.json({ my_customs: myCustoms }) +} ``` -In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to. - -*** +In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key. -## Manage Many-to-Many Relationship +Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties: -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. +- `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. -### Create Associations +The method returns an object that has a `data` property, which holds an array of the retrieved data. For example: -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. +```json title="Returned Data" +{ + "data": [ + { + "id": "123", + "name": "test" + } + ] +} +``` -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: +*** -```ts highlights={manyHighlights} -// when creating a product -const product = await helloModuleService.createProducts({ - // other properties... - orders: ["123", "321"], -}) +## Querying the Graph -// when creating an order -const order = await helloModuleService.createOrders({ - id: "321", - // other properties... - products: ["123", "321"], -}) -``` +When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates. -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. +This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them. -### Update Associations +*** -When you use the `update` methods generated by the service factory, you also pass an array of IDs as the relation property's value to add new associated records. +## Retrieve Linked Records -However, this removes any existing associations to records whose IDs aren't included in the array. +Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`. -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: +For example: -```ts -const product = await helloModuleService.updateProducts({ - id: "123", - // other properties... - orders: ["321"], +```ts highlights={[["6"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: [ + "id", + "name", + "product.*", + ], }) ``` -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. +`.*` 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`. -So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product: +### Retrieve List Link Records -```ts highlights={updateAssociationHighlights} -const product = await helloModuleService.retrieveProduct( - "123", - { - relations: ["orders"], - } -) +If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`. -const updatedProduct = await helloModuleService.updateProducts({ - id: product.id, - // other properties... - orders: [ - ...product.orders.map((order) => order.id), - "321", +For example: + +```ts highlights={[["6"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: [ + "id", + "name", + "products.*", ], }) ``` -This keeps existing associations between the product and orders, and adds a new one. - -*** - -## Manage Many-to-Many Relationship with pivotEntity - -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. - -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. +### Apply Filters and Pagination on Linked Records -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: +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. -```ts highlights={["4"]} -class HelloModuleService extends MedusaService({ - Order, - Product, - OrderProduct, -}) {} -``` +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. -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. +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 -// create order-product association -const orderProduct = await helloModuleService.createOrderProducts({ - order_id: "123", - product_id: "123", - metadata: { - test: true, - }, -}) +```ts highlights={queryLinkTableHighlights} +import productCustomLink from "../../../links/product-custom" -// update order-product association -const orderProduct = await helloModuleService.updateOrderProducts({ - id: "123", - metadata: { - test: false, +// ... + +const { data: productCustoms } = await query.graph({ + entity: productCustomLink.entryPoint, + fields: ["*", "product.*", "my_custom.*"], + pagination: { + take: 5, + skip: 0, }, }) - -// delete order-product association -await helloModuleService.deleteOrderProducts("123") ``` -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 +In the object passed to the `graph` method: -The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options. +- 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. -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. +You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination). -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: +The returned `data` is similar to the following: -```ts highlights={retrieveHighlights} -const product = await helloModuleService.retrieveProducts( - "123", - { - relations: ["orders"], +```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... } -) +}] ``` -In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. +*** +## Apply Filters -# Data Model’s Primary Key +```ts highlights={[["6"], ["7"], ["8"], ["9"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + filters: { + id: [ + "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX", + "mc_01HWSVWK3KYHKQEE6QGS2JC3FX", + ], + }, +}) +``` -In this chapter, you’ll learn how to configure the primary key of a data model. +The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records. -## primaryKey Method +In the example above, you filter the `my_custom` records by multiple IDs. -To set any `id`, `text`, or `number` property as a primary key, use the `primaryKey` method. +Filters don't apply on fields of linked data models from other modules. -For example: +*** -```ts highlights={highlights} -import { model } from "@medusajs/framework/utils" +## Apply Pagination -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - // ... +```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, + }, }) - -export default MyCustom ``` -In the example above, the `id` property is defined as the data model's primary key. - - -# Data Model Property Types +The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records. -In this chapter, you’ll learn about the types of properties in a data model’s schema. +To paginate the returned records, pass the following properties to `pagination`: -## id +- `skip`: (required to apply pagination) The number of records to skip before fetching the results. +- `take`: The number of records to fetch. -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. +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: -For example: +- skip: (\`number\`) The number of records skipped. +- take: (\`number\`) The number of records requested to fetch. +- count: (\`number\`) The total number of records. -```ts highlights={idHighlights} -import { model } from "@medusajs/framework/utils" +### Sort Records -const MyCustom = model.define("my_custom", { - id: model.id(), - // ... +```ts highlights={[["5"], ["6"], ["7"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + pagination: { + order: { + name: "DESC", + }, + }, }) - -export default MyCustom ``` -*** - -## text +Sorting doesn't work on fields of linked data models from other modules. -The `text` method defines a string property. +To sort returned records, pass an `order` property to `pagination`. -For example: +The `order` property is an object whose keys are property names, and values are either: -```ts highlights={textHighlights} -import { model } from "@medusajs/framework/utils" +- `ASC` to sort records by that property in ascending order. +- `DESC` to sort records by that property in descending order. -const MyCustom = model.define("my_custom", { - name: model.text(), - // ... -}) +*** -export default MyCustom -``` +## Request Query Configurations -*** +For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that: -## number +- 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. -The `number` method defines a number property. +Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request. -For example: +### Step 1: Add Middleware -```ts highlights={numberHighlights} -import { model } from "@medusajs/framework/utils" +The first step is to use the `validateAndTransformQuery` middleware on the `GET` route. You add the middleware in `src/api/middlewares.ts`: -const MyCustom = model.define("my_custom", { - age: model.number(), - // ... -}) +```ts title="src/api/middlewares.ts" +import { + validateAndTransformQuery, + defineMiddlewares, +} from "@medusajs/framework/http" +import { createFindParams } from "@medusajs/medusa/api/utils/validators" -export default MyCustom -``` +export const GetCustomSchema = createFindParams() -*** +export default defineMiddlewares({ + routes: [ + { + matcher: "/customs", + method: "GET", + middlewares: [ + validateAndTransformQuery( + GetCustomSchema, + { + defaults: [ + "id", + "name", + "products.*", + ], + isList: true, + } + ), + ], + }, + ], +}) +``` -## float +The `validateAndTransformQuery` accepts two parameters: -This property is only available after [Medusa v2.1.2](https://github.com/medusajs/medusa/releases/tag/v2.1.2). +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`. -The `float` method defines a number property that allows for values with decimal places. +### Step 2: Use Configurations in API Route -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). +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={floatHighlights} -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 MyCustom = model.define("my_custom", { - rating: model.float(), - // ... -}) +For example, Create the file `src/api/customs/route.ts` with the following content: -export default MyCustom -``` +```ts title="src/api/customs/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -*** +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) -## bigNumber + const { data: myCustoms } = await query.graph({ + entity: "my_custom", + ...req.queryConfig, + }) -The `bigNumber` method defines a number property that expects large numbers, such as prices. + res.json({ my_customs: myCustoms }) +} +``` -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). +This adds a `GET` API route at `/customs`, which is the API route you added the middleware for. -For example: +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. -```ts highlights={bigNumberHighlights} -import { model } from "@medusajs/framework/utils" +### Test it Out -const MyCustom = model.define("my_custom", { - price: model.bigNumber(), - // ... -}) +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. -export default MyCustom +```json title="Returned Data" +{ + "my_customs": [ + { + "id": "123", + "name": "test" + } + ] +} ``` -*** +Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result. -## boolean +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. -The `boolean` method defines a boolean property. -For example: +# Query Context -```ts highlights={booleanHighlights} -import { model } from "@medusajs/framework/utils" +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). -const MyCustom = model.define("my_custom", { - hasAccount: model.boolean(), - // ... -}) +## What is Query Context? -export default MyCustom -``` +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). *** -### enum +## How to Use Query Context -The `enum` method defines a property whose value can only be one of the specified values. +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`). -For example: +You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument. -```ts highlights={enumHighlights} -import { model } from "@medusajs/framework/utils" +For example, to retrieve posts using Query while passing the user's language as a context: -const MyCustom = model.define("my_custom", { - color: model.enum(["black", "white"]), - // ... +```ts +const { data } = await query.graph({ + entity: "post", + fields: ["*"], + context: QueryContext({ + lang: "es", + }), }) - -export default MyCustom ``` -The `enum` method accepts an array of possible string values. +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. -## dateTime +For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context: -The `dateTime` method defines a timestamp property. +```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" -For example: +class BlogModuleService extends MedusaService({ + Post, + Author, +}){ + // @ts-ignore + async listPosts( + filters?: any, + config?: FindConfig<any> | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context -```ts highlights={dateTimeHighlights} -import { model } from "@medusajs/framework/utils" + let posts = await super.listPosts(filters, config, sharedContext) -const MyCustom = model.define("my_custom", { - date_of_birth: model.dateTime(), - // ... -}) + if (context.lang === "es") { + posts = posts.map((post) => { + return { + ...post, + title: post.title + " en español", + } + }) + } -export default MyCustom -``` + return posts + } +} -*** +export default BlogModuleService +``` -## json +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 `json` method defines a property whose value is a stringified JSON object. +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. -For example: +All posts returned will now have their titles appended with "en español". -```ts highlights={jsonHighlights} -import { model } from "@medusajs/framework/utils" +Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md). -const MyCustom = model.define("my_custom", { - metadata: model.json(), - // ... -}) +### Using Pagination with Query -export default MyCustom -``` +If you pass pagination fields to `query.graph`, you must also override the `listAndCount` method in the service. -*** +For example, following along with the previous example, you must override the `listAndCountPosts` method of the Blog Module's service: -## array +```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" -The `array` method defines an array of strings property. +class BlogModuleService extends MedusaService({ + Post, + Author, +}){ + // @ts-ignore + async listAndCountPosts( + filters?: any, + config?: FindConfig<any> | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context -For example: + const result = await super.listAndCountPosts( + filters, + config, + sharedContext + ) -```ts highlights={arrHightlights} -import { model } from "@medusajs/framework/utils" + if (context.lang === "es") { + result.posts = posts.map((post) => { + return { + ...post, + title: post.title + " en español", + } + }) + } -const MyCustom = model.define("my_custom", { - names: model.array(), - // ... -}) + return result + } +} -export default MyCustom +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`. + *** -## Properties Reference +## Passing Query Context to Related Data Models -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. +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. +For linked data models, check out the [next section](#passing-query-context-to-linked-data-models). -# Data Model Relationships +For example, to pass a context for the post's authors: -In this chapter, you’ll learn how to define relationships between data models in your module. +```ts highlights={highlights3} +const { data } = await query.graph({ + entity: "post", + fields: ["*"], + context: QueryContext({ + lang: "es", + author: QueryContext({ + lang: "es", + }), + }), +}) +``` -## What is a Relationship Property? +Then, in the `listPosts` method, you can handle the context for the post's authors: -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`. +```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" -When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type. +class BlogModuleService extends MedusaService({ + Post, + Author, +}){ + // @ts-ignore + async listPosts( + filters?: any, + config?: FindConfig<any> | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context -You want to create a relation between data models in the same module. + let posts = await super.listPosts(filters, config, sharedContext) -You want to create a relationship between data models in different modules. Use module links instead. + const isPostLangEs = context.lang === "es" + const isAuthorLangEs = context.author?.lang === "es" -*** + 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, + }, + } + }) + } -## One-to-One Relationship + return posts + } +} -A one-to-one relationship indicates that one record of a data model belongs to or is associated with another. +export default BlogModuleService +``` -To define a one-to-one relationship, create relationship properties in the data models using the following methods: +The context in `filters` will also have the context for `author`, which you can use to make transformations to the post's authors. -1. `hasOne`: indicates that the model has one record of the specified model. -2. `belongsTo`: indicates that the model belongs to one record of the specified model. +*** -For example: +## Passing Query Context to Linked Data Models -```ts highlights={oneToOneHighlights} -import { model } from "@medusajs/framework/utils" +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. -const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email), -}) +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: -const Email = model.define("email", { - id: model.id().primaryKey(), - user: model.belongsTo(() => User, { - mappedBy: "email", - }), +```ts highlights={highlights5} +const { data } = await query.graph({ + entity: "product", + fields: ["*", "post.*"], + context: { + post: QueryContext({ + lang: "es", + }), + }, }) ``` -In the example above, a user has one email, and an email belongs to one user. - -The `hasOne` and `belongsTo` methods accept a function as 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. +In this example, you retrieve products and their associated posts. You also pass a context for `post`, indicating the customer's language. -### Optional Relationship +To handle the context, you override the generated `listPosts` method of the Blog Module as explained [previously](#how-to-use-query-context). -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). -### One-sided One-to-One Relationship +# Architectural Modules -If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. +In this chapter, you’ll learn about architectural modules. -For example: +## What is an Architectural Module? -```ts highlights={oneToOneUndefinedHighlights} -import { model } from "@medusajs/framework/utils" +An architectural module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. -const User = model.define("user", { - id: model.id().primaryKey(), -}) +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. -const Email = model.define("email", { - id: model.id().primaryKey(), - user: model.belongsTo(() => User, { - mappedBy: undefined, - }), -}) -``` +*** -### One-to-One Relationship in the Database +## Architectural Module Types -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: +There are different architectural module types including: -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. + - +- 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. *** -## One-to-Many Relationship - -A one-to-many relationship indicates that one record of a data model has many records of another data model. - -To define a one-to-many relationship, create relationship properties in the data models using the following methods: - -1. `hasMany`: indicates that the model has more than one record of the specified model. -2. `belongsTo`: indicates that the model belongs to one record of the specified model. +## Architectural Modules List -For example: +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 highlights={oneToManyHighlights} -import { model } from "@medusajs/framework/utils" -const Store = model.define("store", { - id: model.id().primaryKey(), - products: model.hasMany(() => Product), -}) +# Commerce Modules -const Product = model.define("product", { - id: model.id().primaryKey(), - store: model.belongsTo(() => Store, { - mappedBy: "products", - }), -}) -``` +In this chapter, you'll learn about Medusa's commerce modules. -In this example, a store has many products, but a product belongs to one store. +## What is a Commerce Module? -### Optional Relationship +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. -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). +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. -### One-to-Many Relationship in the Database +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) -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: +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. -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. +### List of Medusa's Commerce Modules - +Refer to [this reference](https://docs.medusajs.com/resources/commerce-modules/index.html.md) for a full list of commerce modules in Medusa. *** -## Many-to-Many Relationship +## Use Commerce Modules in Custom Flows -A many-to-many relationship indicates that many records of a data model can be associated with many records of another data model. +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. -To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method. +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: -For example: +```ts highlights={highlights} +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -```ts highlights={manyToManyHighlights} -import { model } from "@medusajs/framework/utils" +export const countProductsStep = createStep( + "count-products", + async ({ }, { container }) => { + const productModuleService = container.resolve("product") -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 [,count] = await productModuleService.listAndCountProducts() -const Product = model.define("product", { - id: model.id().primaryKey(), - orders: model.manyToMany(() => Order, { - mappedBy: "products", - }), -}) + return new StepResponse(count) + } +) ``` -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. +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. -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. +# Module Container -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. +In this chapter, you'll learn about the module's container and how to resolve resources in that container. -### Many-to-Many Relationship in the Database +Since modules are isolated, each module has a local container only used by the resources of that 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`. +So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container. -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. +### List of Registered Resources -The pivot table has columns with foreign keys pointing to the primary key of the associated tables. The column's name is either: +Find a list of resources or dependencies registered in a module's container in [this Development Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/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`. +*** - +## Resolve Resources -### Many-To-Many with Custom Columns +### Services -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. +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={manyToManyColumnHighlights} -import { model } from "@medusajs/framework/utils" +```ts highlights={[["4"], ["10"]]} +import { Logger } from "@medusajs/framework/types" -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), -}) - -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(), -}) -``` - -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: - -- `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. - -*** - -## 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. - -As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. +type InjectedDependencies = { + logger: Logger +} -For example: +export default class HelloModuleService { + protected logger_: Logger -```ts highlights={relationNameHighlights} -import { model } from "@medusajs/framework/utils" + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger -const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email, { - mappedBy: "owner", - }), -}) + this.logger_.info("[HelloModuleService]: Hello World!") + } -const Email = model.define("email", { - id: model.id().primaryKey(), - owner: model.belongsTo(() => User, { - mappedBy: "email", - }), -}) + // ... +} ``` -In this example, you specify in the `User` data model’s relationship property that the name of the relationship in the `Email` data model is `owner`. - -*** - -## Cascades - -When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it. - -For example, if a store is deleted, its products should also be deleted. +### Loader -The `cascades` method used on a data model configures which child records an operation is cascaded 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. For example: -```ts highlights={highlights} -import { model } from "@medusajs/framework/utils" +```ts highlights={[["9"]]} +import { + LoaderOptions, +} from "@medusajs/framework/types" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -const Store = model.define("store", { - id: model.id().primaryKey(), - products: model.hasMany(() => Product), -}) -.cascades({ - delete: ["products"], -}) +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) -const Product = model.define("product", { - id: model.id().primaryKey(), - store: model.belongsTo(() => Store, { - mappedBy: "products", - }), -}) + logger.info("[helloWorldLoader]: Hello, World!") +} ``` -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. - -# Searchable Data Model Property +# Perform Database Operations in a Service -In this chapter, you'll learn what a searchable property is and how to define it. +In this chapter, you'll learn how to perform database operations in a module's service. -## What is a Searchable Property? +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. -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. +## Run Queries -When the `q` filter is passed, the data model's searchable properties are queried to find matching records. +[MikroORM's entity manager](https://mikro-orm.io/docs/entity-manager) is a class that has methods to run queries on the database and perform operations. -*** +Medusa provides an `InjectManager` decorator 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). -## Define a Searchable Property +So, to run database queries in a service: -Use the `searchable` method on a `text` property to indicate that it's searchable. +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` -For example: +For example, in your service, add the following methods: -```ts highlights={searchableHighlights} -import { model } from "@medusajs/framework/utils" +```ts highlights={methodsHighlight} +// other imports... +import { + InjectManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { SqlEntityManager } from "@mikro-orm/knex" -const MyCustom = model.define("my_custom", { - name: model.text().searchable(), +class HelloModuleService { // ... -}) - -export default MyCustom -``` - -In this example, the `name` property is searchable. - -### Search Example - -If you pass a `q` filter to the `listMyCustoms` method: -```ts -const myCustoms = await helloModuleService.listMyCustoms({ - q: "John", -}) + @InjectManager() + async getCount( + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<number> { + return await sharedContext.manager.count("my_custom") + } + + @InjectManager() + async getCountSql( + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<number> { + const data = await sharedContext.manager.execute( + "SELECT COUNT(*) as num FROM my_custom" + ) + + return parseInt(data[0].num) + } +} ``` -This retrieves records that include `John` in their `name` property. - - -# Write Migration - -In this chapter, you'll learn how to create a migration and write it manually. +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. -## What is a Migration? +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). -A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It has two methods: +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. -- The `up` method reflects changes on the database. -- The `down` method reverts the changes made in the `up` method. +Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. *** -## How to Write a Migration? +## Execute Operations in Transactions -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 wrap database operations in a transaction, you create two methods: -Alternatively, you can manually create a migration file under the `migrations` directory of your module. +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. + +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/modules/hello/migrations/Migration20240429.ts" -import { Migration } from "@mikro-orm/migrations" +```ts highlights={opHighlights} +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" -export class Migration20240702105919 extends Migration { +class HelloModuleService { + // ... + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<any> { + const transactionManager = sharedContext.transactionManager + await transactionManager.nativeUpdate( + "my_custom", + { + id: input.id, + }, + { + name: input.name, + } + ) - async up(): Promise<void> { - this.addSql("create table if not exists \"my_custom\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"my_custom_pkey\" primary key (\"id\"));") - } + // retrieve again + const updatedRecord = await transactionManager.execute( + `SELECT * FROM my_custom WHERE id = '${input.id}'` + ) - async down(): Promise<void> { - this.addSql("drop table if exists \"my_custom\" cascade;") + return updatedRecord } + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ) { + return await this.update_(input, sharedContext) + } } ``` -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 `HelloModuleService` has two methods: -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. +- A protected `update_` that performs the database operations inside a transaction. +- A public `update` that executes the transactional protected method. -In the example above, the `up` method creates the table `my_custom`, and the `down` method drops the table if the migration is reverted. +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 documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. +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 -## Run the Migration +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. -To run your migration, run the following command: +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. -This command also syncs module links. If you don't want that, use the `--skip-links` option. +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. -```bash -npx medusa db:migrate -``` +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 reflects the changes in the database as implemented in the migration's `up` method. +For example, the `update` method could be changed to the following: -*** +```ts +// other imports... +import { EntityManager } from "@mikro-orm/knex" -## Rollback the Migration +class HelloModuleService { + // ... + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ) { + const newData = await this.update_(input, sharedContext) -To rollback or revert the last migration you ran for a module, run the following command: + await sendNewDataToSystem(newData) -```bash -npx medusa db:rollback helloModuleService + return newData + } +} ``` -This rolls back the last ran migration on the Hello Module. - -*** - -## More Database Commands - -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). +In this case, only the `update_` method is wrapped in a transaction. The returned value `newData` holds the committed result, which can be used for other operations, such as passed to a `sendNewDataToSystem` method. +### Using Methods in Transactional Methods -# Commerce Modules +If your transactional method uses other methods that accept a Medusa context, pass the shared context to those methods. -In this chapter, you'll learn about Medusa's commerce modules. +For example: -## What is a Commerce Module? +```ts +// other imports... +import { EntityManager } from "@mikro-orm/knex" -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. +class HelloModuleService { + // ... + @InjectTransactionManager() + protected async anotherMethod( + @MedusaContext() sharedContext?: Context<EntityManager> + ) { + // ... + } + + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<any> { + this.anotherMethod(sharedContext) + } +} +``` -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. +You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context. -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) +The `anotherMethod` now runs in the same transaction as the `update_` method. -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. +*** -### List of Medusa's Commerce Modules +## Configure Transactions -Refer to [this reference](https://docs.medusajs.com/resources/commerce-modules/index.html.md) for a full list of commerce modules in Medusa. +To configure the transaction, such as its [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html), use the `baseRepository` dependency registered in your module's container. -*** +The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more. -## Use Commerce Modules in Custom Flows +The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction. -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. +For example, resolve the `baseRepository` in your service's constructor: -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: +### Extending Service Factory -```ts highlights={highlights} -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +```ts highlights={[["14"]]} +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" +import { DAL } from "@medusajs/framework/types" -export const countProductsStep = createStep( - "count-products", - async ({ }, { container }) => { - const productModuleService = container.resolve("product") +type InjectedDependencies = { + baseRepository: DAL.RepositoryService +} - const [,count] = await productModuleService.listAndCountProducts() +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + protected baseRepository_: DAL.RepositoryService - return new StepResponse(count) + constructor({ baseRepository }: InjectedDependencies) { + super(...arguments) + this.baseRepository_ = baseRepository } -) -``` +} -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 HelloModuleService +``` +### Without Service Factory -# Architectural Modules +```ts highlights={[["10"]]} +import { DAL } from "@medusajs/framework/types" -In this chapter, you’ll learn about architectural modules. +type InjectedDependencies = { + baseRepository: DAL.RepositoryService +} -## What is an Architectural Module? +class HelloModuleService { + protected baseRepository_: DAL.RepositoryService -An architectural module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. + constructor({ baseRepository }: InjectedDependencies) { + this.baseRepository_ = baseRepository + } +} -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. +export default HelloModuleService +``` -*** +Then, add the following method that uses it: -## Architectural Module Types +```ts highlights={repoHighlights} +// ... +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" -There are different architectural module types including: +class HelloModuleService { + // ... + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<any> { + return await this.baseRepository_.transaction( + async (transactionManager) => { + await transactionManager.nativeUpdate( + "my_custom", + { + id: input.id, + }, + { + name: input.name, + } + ) - + // retrieve again + const updatedRecord = await transactionManager.execute( + `SELECT * FROM my_custom WHERE id = '${input.id}'` + ) -- 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. + return updatedRecord + }, + { + transaction: sharedContext.transactionManager, + } + ) + } -*** + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ) { + return await this.update_(input, sharedContext) + } +} +``` -## Architectural Modules List +The `update_` method uses the `baseRepository_.transaction` method to wrap a function in a transaction. -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. +The function parameter receives a transactional entity manager as a parameter. Use it to perform the database operations. +The `baseRepository_.transaction` method also receives as a second parameter an object of options. You must pass in it the `transaction` property and set its value to the `sharedContext.transactionManager` property so that the function wrapped in the transaction uses the injected transaction manager. -# Module Container +Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. -In this chapter, you'll learn about the module's container and how to resolve resources in that container. +### Transaction Options -Since modules are isolated, each module has a local container only used by the resources of that module. +The second parameter of the `baseRepository_.transaction` method is an object of options that accepts the following properties: -So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container. +1. `transaction`: Set the transactional entity manager passed to the function. You must provide this option as explained in the previous section. -### List of Registered Resources +```ts highlights={[["16"]]} +// other imports... +import { EntityManager } from "@mikro-orm/knex" -Find a list of resources or dependencies registered in a module's container in [this Development Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). +class HelloModuleService { + // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<any> { + return await this.baseRepository_.transaction<EntityManager>( + async (transactionManager) => { + // ... + }, + { + transaction: sharedContext.transactionManager, + } + ) + } +} +``` -*** +2. `isolationLevel`: Sets the transaction's [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html). Its values can be: + - `read committed` + - `read uncommitted` + - `snapshot` + - `repeatable read` + - `serializable` -## Resolve Resources - -### Services - -A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. - -For example: - -```ts highlights={[["4"], ["10"]]} -import { Logger } from "@medusajs/framework/types" - -type InjectedDependencies = { - logger: Logger -} - -export default class HelloModuleService { - protected logger_: Logger - - constructor({ logger }: InjectedDependencies) { - this.logger_ = logger - - this.logger_.info("[HelloModuleService]: Hello World!") - } +```ts highlights={[["19"]]} +// other imports... +import { IsolationLevel } from "@mikro-orm/core" +class HelloModuleService { // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<any> { + return await this.baseRepository_.transaction<EntityManager>( + async (transactionManager) => { + // ... + }, + { + isolationLevel: IsolationLevel.READ_COMMITTED, + } + ) + } } ``` -### Loader - -A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources. - -For example: - -```ts highlights={[["9"]]} -import { - LoaderOptions, -} from "@medusajs/framework/types" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" - -export default async function helloWorldLoader({ - container, -}: LoaderOptions) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) +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. - logger.info("[helloWorldLoader]: Hello, World!") +```ts highlights={[["16"]]} +class HelloModuleService { + // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<any> { + return await this.baseRepository_.transaction<EntityManager>( + async (transactionManager) => { + // ... + }, + { + enableNestedTransactions: false, + } + ) + } } ``` @@ -10630,6 +11053,33 @@ export const syncBrandsWorkflow = createWorkflow( You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. +# Modules Directory Structure + +In this document, you'll learn about the expected files and directories in your module. + + + +## index.ts + +The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +*** + +## service.ts + +A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +*** + +## Other Directories + +The following directories are optional and their content are explained more in the following chapters: + +- `models`: Holds the data models representing tables in the database. +- `migrations`: Holds the migration files used to reflect changes on the database. +- `loaders`: Holds the scripts to run on the Medusa application's start-up. + + # Loaders In this chapter, you’ll learn about loaders and how to use them. @@ -10873,581 +11323,308 @@ info: Connected to MongoDB You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database. -# Perform Database Operations in a Service - -In this chapter, you'll learn how to perform database operations in a module's service. +# Multiple Services in a Module -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. +In this chapter, you'll learn how to use multiple services in a module. -## Run Queries +## Module's Main and Internal Services -[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. +A module has one main service only, which is the service exported in the module's definition. -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). +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. -So, to run database queries in a service: +*** -1. Add the `InjectManager` decorator to the method. -2. Add as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator from the Modules SDK. This context holds database-related context, including the manager injected by `InjectManager` +## How to Add an Internal Service -For example, in your service, add the following methods: +### 1. Create Service -```ts highlights={methodsHighlight} -// other imports... -import { - InjectManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { SqlEntityManager } from "@mikro-orm/knex" +To add an internal service, create it in the `services` directory of your module. -class HelloModuleService { - // ... +For example, create the file `src/modules/hello/services/client.ts` with the following content: - @InjectManager() - async getCount( - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<number> { - return await sharedContext.manager.count("my_custom") - } - - @InjectManager() - async getCountSql( - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<number> { - const data = await sharedContext.manager.execute( - "SELECT COUNT(*) as num FROM my_custom" - ) - - return parseInt(data[0].num) +```ts title="src/modules/hello/services/client.ts" +export class ClientService { + async getMessage(): Promise<string> { + return "Hello, World!" } } ``` -You add two methods `getCount` and `getCountSql` that have the `InjectManager` decorator. Each of the methods also accept the `sharedContext` parameter which has the `MedusaContext` decorator. - -The entity manager is injected to the `sharedContext.manager` property, which is an instance of [EntityManager from the @mikro-orm/knex package](https://mikro-orm.io/api/knex/class/EntityManager). - -You use the manager in the `getCount` method to retrieve the number of records in a table, and in the `getCountSql` to run a PostgreSQL query that retrieves the count. - -Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. +### 2. Export Service in Index -*** +Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. -## Execute Operations in Transactions +For example, create the file `src/modules/hello/services/index.ts` with the following content: -To wrap database operations in a transaction, you create two methods: +```ts title="src/modules/hello/services/index.ts" +export * from "./client" +``` -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. +This exports the `ClientService`. -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. +### 3. Resolve Internal Service -For example: +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. -```ts highlights={opHighlights} -import { - InjectManager, - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" +For example, in your main service: -class HelloModuleService { - // ... - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<any> { - const transactionManager = sharedContext.transactionManager - await transactionManager.nativeUpdate( - "my_custom", - { - id: input.id, - }, - { - name: input.name, - } - ) +```ts title="src/modules/hello/service.ts" highlights={[["5"], ["13"]]} +// other imports... +import { ClientService } from "./services" - // retrieve again - const updatedRecord = await transactionManager.execute( - `SELECT * FROM my_custom WHERE id = '${input.id}'` - ) +type InjectedDependencies = { + clientService: ClientService +} - return updatedRecord - } +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + protected clientService_: ClientService - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ) { - return await this.update_(input, sharedContext) + constructor({ clientService }: InjectedDependencies) { + super(...arguments) + this.clientService_ = clientService } } ``` -The `HelloModuleService` has two methods: - -- A protected `update_` that performs the database operations inside a transaction. -- A public `update` that executes the transactional protected method. - -The shared context's `transactionManager` property holds the transactional entity manager (injected by `InjectTransactionManager`) that you use to perform database operations. - -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 - -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. +You can now use your internal service in your main service. -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. +*** -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. +## Resolve Resources in Internal Service -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. +Resolve dependencies from your module's container in the constructor of your internal service. -For example, the `update` method could be changed to the following: +For example: ```ts -// other imports... -import { EntityManager } from "@mikro-orm/knex" +import { Logger } from "@medusajs/framework/types" -class HelloModuleService { - // ... - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ) { - const newData = await this.update_(input, sharedContext) +type InjectedDependencies = { + logger: Logger +} - await sendNewDataToSystem(newData) +export class ClientService { + protected logger_: Logger - return newData + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger } } ``` -In this case, only the `update_` method is wrapped in a transaction. The returned value `newData` holds the committed result, which can be used for other operations, such as passed to a `sendNewDataToSystem` method. +*** -### Using Methods in Transactional Methods +## Access Module Options -If your transactional method uses other methods that accept a Medusa context, pass the shared context to those methods. +Your internal service can't access the module's options. + +To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`. For example: ```ts -// other imports... -import { EntityManager } from "@mikro-orm/knex" +import { ConfigModule } from "@medusajs/framework/types" +import { HELLO_MODULE } from ".." -class HelloModuleService { - // ... - @InjectTransactionManager() - protected async anotherMethod( - @MedusaContext() sharedContext?: Context<EntityManager> - ) { - // ... - } - - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<any> { - this.anotherMethod(sharedContext) +export type InjectedDependencies = { + configModule: ConfigModule +} + +export class ClientService { + protected options: Record<string, any> + + constructor({ configModule }: InjectedDependencies) { + const moduleDef = configModule.modules[HELLO_MODULE] + + if (typeof moduleDef !== "boolean") { + this.options = moduleDef.options + } } } ``` -You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context. +The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. -The `anotherMethod` now runs in the same transaction as the `update_` method. +If its value is not a `boolean`, set the service's options to the module configuration's `options` property. -*** -## Configure Transactions +# Service Factory -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. +In this chapter, you’ll learn about what the service factory is and how to use it. -The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more. +## What is the Service Factory? -The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction. +Medusa provides a service factory that your module’s main service can extend. -For example, resolve the `baseRepository` in your service's constructor: +The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually. -### Extending Service Factory +Your service provides data-management functionalities of your data models. -```ts highlights={[["14"]]} +*** + +## How to Extend the Service Factory? + +Medusa provides the service factory as a `MedusaService` function your service extends. The function creates and returns a service class with generated data-management methods. + +For example, create the file `src/modules/hello/service.ts` with the following content: + +```ts title="src/modules/hello/service.ts" highlights={highlights} import { MedusaService } from "@medusajs/framework/utils" import MyCustom from "./models/my-custom" -import { DAL } from "@medusajs/framework/types" - -type InjectedDependencies = { - baseRepository: DAL.RepositoryService -} class HelloModuleService extends MedusaService({ MyCustom, }){ - protected baseRepository_: DAL.RepositoryService - - constructor({ baseRepository }: InjectedDependencies) { - super(...arguments) - this.baseRepository_ = baseRepository - } + // TODO implement custom methods } export default HelloModuleService ``` -### Without Service Factory +### MedusaService Parameters -```ts highlights={[["10"]]} -import { DAL } from "@medusajs/framework/types" +The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for. -type InjectedDependencies = { - baseRepository: DAL.RepositoryService -} +In the example above, since the `HelloModuleService` extends `MedusaService`, it has methods to manage the `MyCustom` data model, such as `createMyCustoms`. -class HelloModuleService { - protected baseRepository_: DAL.RepositoryService +### Generated Methods - constructor({ baseRepository }: InjectedDependencies) { - this.baseRepository_ = baseRepository - } -} +The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database. -export default HelloModuleService -``` +The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`. -Then, add the following method that uses it: +For example, the following methods are generated for the service above: -```ts highlights={repoHighlights} -// ... -import { - InjectManager, - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" +Find a complete reference of each of the methods in [this documentation](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) -class HelloModuleService { - // ... - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<any> { - return await this.baseRepository_.transaction( - async (transactionManager) => { - await transactionManager.nativeUpdate( - "my_custom", - { - id: input.id, - }, - { - name: input.name, - } - ) +### listMyCustoms - // retrieve again - const updatedRecord = await transactionManager.execute( - `SELECT * FROM my_custom WHERE id = '${input.id}'` - ) +### listMyCustoms - return updatedRecord - }, - { - transaction: sharedContext.transactionManager, - } - ) - } +This method retrieves an array of records based on filters and pagination configurations. - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ) { - return await this.update_(input, sharedContext) - } -} -``` +For example: -The `update_` method uses the `baseRepository_.transaction` method to wrap a function in a transaction. +```ts +const myCustoms = await helloModuleService + .listMyCustoms() -The function parameter receives a transactional entity manager as a parameter. Use it to perform the database operations. +// with filters +const myCustoms = await helloModuleService + .listMyCustoms({ + id: ["123"] + }) +``` -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. +### listAndCount -Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. +### retrieveMyCustom -### Transaction Options +This method retrieves a record by its ID. -The second parameter of the `baseRepository_.transaction` method is an object of options that accepts the following properties: +For example: -1. `transaction`: Set the transactional entity manager passed to the function. You must provide this option as explained in the previous section. +```ts +const myCustom = await helloModuleService + .retrieveMyCustom("123") +``` -```ts highlights={[["16"]]} -// other imports... -import { EntityManager } from "@mikro-orm/knex" +### retrieveMyCustom -class HelloModuleService { - // ... - @InjectTransactionManager() - async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<any> { - return await this.baseRepository_.transaction<EntityManager>( - async (transactionManager) => { - // ... - }, - { - transaction: sharedContext.transactionManager, - } - ) - } -} -``` +### updateMyCustoms -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` +This method updates and retrieves records of the data model. -```ts highlights={[["19"]]} -// other imports... -import { IsolationLevel } from "@mikro-orm/core" +For example: -class HelloModuleService { - // ... - @InjectTransactionManager() - async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<any> { - return await this.baseRepository_.transaction<EntityManager>( - async (transactionManager) => { - // ... - }, - { - isolationLevel: IsolationLevel.READ_COMMITTED, - } - ) - } -} -``` - -3. `enableNestedTransactions`: (default: `false`) whether to allow using nested transactions. - - If `transaction` is provided and this is disabled, the manager in `transaction` is re-used. +```ts +const myCustom = await helloModuleService + .updateMyCustoms({ + id: "123", + name: "test" + }) -```ts highlights={[["16"]]} -class HelloModuleService { - // ... - @InjectTransactionManager() - async update_( - input: { - id: string, - name: string +// update multiple +const myCustoms = await helloModuleService + .updateMyCustoms([ + { + id: "123", + name: "test" }, - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<any> { - return await this.baseRepository_.transaction<EntityManager>( - async (transactionManager) => { - // ... + { + id: "321", + name: "test 2" + }, + ]) + +// use filters +const myCustoms = await helloModuleService + .updateMyCustoms([ + { + selector: { + id: ["123", "321"] }, - { - enableNestedTransactions: false, + data: { + name: "test" } - ) - } -} + }, + ]) ``` +### createMyCustoms -# Modules Directory Structure - -In this document, you'll learn about the expected files and directories in your module. - - - -## index.ts - -The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -*** - -## service.ts - -A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -*** - -## Other Directories - -The following directories are optional and their content are explained more in the following chapters: - -- `models`: Holds the data models representing tables in the database. -- `migrations`: Holds the migration files used to reflect changes on the database. -- `loaders`: Holds the scripts to run on the Medusa application's start-up. - - -# Multiple Services in a Module - -In this chapter, you'll learn how to use multiple services in a module. - -## Module's Main and Internal Services - -A module has one main service only, which is the service exported in the module's definition. - -However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources. - -*** +### softDeleteMyCustoms -## How to Add an Internal Service +This method soft-deletes records using an array of IDs or an object of filters. -### 1. Create Service +For example: -To add an internal service, create it in the `services` directory of your module. +```ts +await helloModuleService.softDeleteMyCustoms("123") -For example, create the file `src/modules/hello/services/client.ts` with the following content: +// soft-delete multiple +await helloModuleService.softDeleteMyCustoms([ + "123", "321" +]) -```ts title="src/modules/hello/services/client.ts" -export class ClientService { - async getMessage(): Promise<string> { - return "Hello, World!" - } -} +// use filters +await helloModuleService.softDeleteMyCustoms({ + id: ["123", "321"] +}) ``` -### 2. Export Service in Index - -Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. - -For example, create the file `src/modules/hello/services/index.ts` with the following content: +### updateMyCustoms -```ts title="src/modules/hello/services/index.ts" -export * from "./client" -``` +### deleteMyCustoms -This exports the `ClientService`. +### softDeleteMyCustoms -### 3. Resolve Internal Service +### restoreMyCustoms -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. +### Using a Constructor -For example, in your main service: +If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`. -```ts title="src/modules/hello/service.ts" highlights={[["5"], ["13"]]} -// other imports... -import { ClientService } from "./services" +For example: -type InjectedDependencies = { - clientService: ClientService -} +```ts highlights={[["8"]]} +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" class HelloModuleService extends MedusaService({ MyCustom, }){ - protected clientService_: ClientService - - constructor({ clientService }: InjectedDependencies) { + constructor() { super(...arguments) - this.clientService_ = clientService - } -} -``` - -You can now use your internal service in your main service. - -*** - -## Resolve Resources in Internal Service - -Resolve dependencies from your module's container in the constructor of your internal service. - -For example: - -```ts -import { Logger } from "@medusajs/framework/types" - -type InjectedDependencies = { - logger: Logger -} - -export class ClientService { - protected logger_: Logger - - constructor({ logger }: InjectedDependencies) { - this.logger_ = logger } } -``` - -*** - -## Access Module Options - -Your internal service can't access the module's options. - -To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`. - -For example: - -```ts -import { ConfigModule } from "@medusajs/framework/types" -import { HELLO_MODULE } from ".." - -export type InjectedDependencies = { - configModule: ConfigModule -} - -export class ClientService { - protected options: Record<string, any> - - constructor({ configModule }: InjectedDependencies) { - const moduleDef = configModule.modules[HELLO_MODULE] - if (typeof moduleDef !== "boolean") { - this.options = moduleDef.options - } - } -} +export default HelloModuleService ``` -The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. - -If its value is not a `boolean`, set the service's options to the module configuration's `options` property. - # Service Constraints @@ -11650,497 +11827,1089 @@ export default Module("hello", { 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. -# 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. - -*** +# Scheduled Jobs Number of Executions -## How to Extend the Service Factory? +In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. -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. +## numberOfExecutions Option -For example, create the file `src/modules/hello/service.ts` with the following content: +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. -```ts title="src/modules/hello/service.ts" highlights={highlights} -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" +For example: -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - // TODO implement custom methods +```ts highlights={highlights} +export default async function myCustomJob() { + console.log("I'll be executed three times only.") } -export default HelloModuleService +export const config = { + name: "hello-world", + // execute every minute + schedule: "* * * * *", + numberOfExecutions: 3, +} ``` -### 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`. +The above scheduled job has the `numberOfExecutions` configuration set to `3`. -### Generated Methods +So, it'll only execute 3 times, each every minute, then it won't be executed anymore. -The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database. +If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. -The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`. -For example, the following methods are generated for the service above: +# Example: Write Integration Tests for API Routes -Find a complete reference of each of the methods in [this documentation](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) +In this chapter, you'll learn how to write integration tests for API routes using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framework. -### listMyCustoms +### Prerequisites -### listMyCustoms +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -This method retrieves an array of records based on filters and pagination configurations. +## Test a GET API Route -For example: +Consider the following API route created at `src/api/custom/route.ts`: -```ts -const myCustoms = await helloModuleService - .listMyCustoms() +```ts title="src/api/custom/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -// with filters -const myCustoms = await helloModuleService - .listMyCustoms({ - id: ["123"] +export async function GET( + req: MedusaRequest, + res: MedusaResponse +){ + res.json({ + message: "Hello, World!", }) +} ``` -### listAndCount - -### retrieveMyCustom +To write an integration test that tests this API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content: -This method retrieves a record by its ID. +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={getHighlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -For example: +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + describe("GET /custom", () => { + it("returns correct message", async () => { + const response = await api.get( + `/custom` + ) + + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("message") + expect(response.data.message).toEqual("Hello, World!") + }) + }) + }) + }, +}) -```ts -const myCustom = await helloModuleService - .retrieveMyCustom("123") +jest.setTimeout(60 * 1000) ``` -### retrieveMyCustom - -### updateMyCustoms +You use the `medusaIntegrationTestRunner` to write your tests. -This method updates and retrieves records of the data model. +You add a single test that sends a `GET` request to `/custom` using the `api.get` method. For the test to pass, the response is expected to: -For example: +- Have a code status `200`, +- Have a `message` property in the returned data. +- Have the value of the `message` property equal to `Hello, World!`. -```ts -const myCustom = await helloModuleService - .updateMyCustoms({ - id: "123", - name: "test" - }) +### Run Tests -// update multiple -const myCustoms = await helloModuleService - .updateMyCustoms([ - { - id: "123", - name: "test" - }, - { - id: "321", - name: "test 2" - }, - ]) +Run the following command to run your tests: -// use filters -const myCustoms = await helloModuleService - .updateMyCustoms([ - { - selector: { - id: ["123", "321"] - }, - data: { - name: "test" - } - }, - ]) +```bash npm2yarn +npm run test:integration ``` -### createMyCustoms - -### softDeleteMyCustoms - -This method soft-deletes records using an array of IDs or an object of filters. +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). -For example: +This runs your Medusa application and runs the tests available under the `src/integrations/http` directory. -```ts -await helloModuleService.softDeleteMyCustoms("123") +### Jest Timeout -// soft-delete multiple -await helloModuleService.softDeleteMyCustoms([ - "123", "321" -]) +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: -// use filters -await helloModuleService.softDeleteMyCustoms({ - id: ["123", "321"] -}) +```ts title="integration-tests/http/custom-routes.spec.ts" +// in your test's file +jest.setTimeout(60 * 1000) ``` -### updateMyCustoms - -### deleteMyCustoms - -### softDeleteMyCustoms - -### restoreMyCustoms - -### Using a Constructor +*** -If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`. +## Test a POST API Route -For example: +Suppose you have a `hello` module whose main service extends the service factory, and that has the following model: -```ts highlights={[["8"]]} -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" +```ts title="src/modules/hello/models/my-custom.ts" +import { model } from "@medusajs/framework/utils" -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - constructor() { - super(...arguments) - } -} +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), +}) -export default HelloModuleService +export default MyCustom ``` +And consider that the file `src/api/custom/route.ts` defines another route handler for `POST` requests: -# 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? - -By default, when an error occurs in a workflow, it throws that error, and the execution stops. - -You can configure the workflow to return the errors instead so that you can access and handle them differently. - -For example: +```ts title="src/api/custom/route.ts" +// other imports... +import HelloModuleService from "../../../modules/hello/service" -```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import myWorkflow from "../../../workflows/hello-world" +// ... -export async function GET( +export async function POST( req: MedusaRequest, res: MedusaResponse ) { - const { result, errors } = await myWorkflow(req.scope) - .run({ - // ... - throwOnError: false, - }) + const helloModuleService: HelloModuleService = req.scope.resolve( + "helloModuleService" + ) - if (errors.length) { - return res.send({ - errors: errors.map((error) => error.error), - }) - } + const myCustom = await helloModuleService.createMyCustoms( + req.body + ) - res.send(result) + res.json({ + my_custom: myCustom, + }) } - ``` -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. +This API route creates a new record of `MyCustom`. -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. +To write tests for this API route, add the following at the end of the `testSuite` function in `integration-tests/http/custom-routes.spec.ts`: +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={postHighlights} +// other imports... +import HelloModuleService from "../../src/modules/hello/service" -# Expose a Workflow Hook +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + // other tests... -In this chapter, you'll learn how to expose a hook in your workflow. + describe("POST /custom", () => { + const id = "1" -## When to Expose a Hook + it("Creates my custom", async () => { + + const response = await api.post( + `/custom`, + { + id, + name: "Test", + } + ) + + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("my_custom") + expect(response.data.my_custom).toEqual({ + id, + name: "Test", + created_at: expect.any(String), + updated_at: expect.any(String), + }) + }) + }) + }) + }, +}) +``` -Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow. +This adds a test for the `POST /custom` API route. It uses `api.post` to send the POST request. The `api.post` method accepts as a second parameter the data to pass in the request body. -Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead. +The test passes if the response has: -*** +- Status code `200`. +- A `my_custom` property in its data. +- Its `id` and `name` match the ones provided to the request. -## How to Expose a Hook in a Workflow? +### Tear Down Created Record -To expose a hook in your workflow, use `createHook` from the Workflows SDK. +To ensure consistency in the database for the rest of the tests after the above test is executed, utilize [Jest's setup and teardown hooks](https://jestjs.io/docs/setup-teardown) to delete the created record. -For example: +Use the `getContainer` function passed as a parameter to the `testSuite` function to resolve a service and use it for setup or teardown purposes -```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" +So, add an `afterAll` hook in the `describe` block for `POST /custom`: -export const myWorkflow = createWorkflow( - "my-workflow", - function (input) { - const product = createProductStep(input) - const productCreatedHook = createHook( - "productCreated", - { productId: product.id } - ) +```ts title="integration-tests/http/custom-routes.spec.ts" +// other imports... +import HelloModuleService from "../../src/modules/hello/service" - return new WorkflowResponse(product, { - hooks: [productCreatedHook], +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + // other tests... + + describe("POST /custom", () => { + // ... + afterAll(() => async () => { + const helloModuleService: HelloModuleService = getContainer().resolve( + "helloModuleService" + ) + + await helloModuleService.deleteMyCustoms(id) + }) + }) }) - } -) + }, +}) ``` -The `createHook` function accepts two parameters: - -1. The first is a string indicating the hook's name. You use this to consume the hook later. -2. The second is the input to pass to the hook handler. +The `afterAll` hook resolves the `HelloModuleService` and use its `deleteMyCustoms` to delete the record created by the test. -The workflow must also pass an object having a `hooks` property as a second parameter to the `WorkflowResponse` constructor. Its value is an array of the workflow's hooks. +*** -### How to Consume the Hook? +## Test a DELETE API Route -To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content: +Consider a `/custom/:id` API route created at `src/api/custom/[id]/route.ts`: -```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights} -import { myWorkflow } from "../my-workflow" +```ts title="src/api/custom/[id]/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import HelloModuleService from "../../../modules/hello/service" -myWorkflow.hooks.productCreated( - async ({ productId }, { container }) => { - // TODO perform an action - } -) -``` +export async function DELETE( + req: MedusaRequest, + res: MedusaResponse +) { + const helloModuleService: HelloModuleService = req.scope.resolve( + "helloModuleService" + ) -The hook is available on the workflow's `hooks` property using its name `productCreated`. + await helloModuleService.deleteMyCustoms(req.params.id) -You invoke the hook, passing a step function (the hook handler) as a parameter. + res.json({ + success: true, + }) +} +``` +This API route accepts an ID path parameter, and uses the `HelloModuleService` to delete a `MyCustom` record by that ID. -# Scheduled Jobs Number of Executions +To add tests for this API route, add the following to `integration-tests/http/custom-routes.spec.ts`: -In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={deleteHighlights} +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + // ... -## numberOfExecutions Option + describe("DELETE /custom/:id", () => { + const id = "1" -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. + beforeAll(() => async () => { + const helloModuleService: HelloModuleService = getContainer().resolve( + "helloModuleService" + ) -For example: + await helloModuleService.createMyCustoms({ + id, + name: "Test", + }) + }) -```ts highlights={highlights} -export default async function myCustomJob() { - console.log("I'll be executed three times only.") -} + it("Deletes my custom", async () => { + const response = await api.delete( + `/custom/${id}` + ) -export const config = { - name: "hello-world", - // execute every minute - schedule: "* * * * *", - numberOfExecutions: 3, -} + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("success") + expect(response.data.success).toBeTruthy() + }) + }) + }) + }, +}) ``` -The above scheduled job has the `numberOfExecutions` configuration set to `3`. - -So, it'll only execute 3 times, each every minute, then it won't be executed anymore. +This adds a new test for the `DELETE /custom/:id` API route. You use the `beforeAll` hook to create a `MyCustom` record using the `HelloModuleService`. -If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. +In the test, you use the `api.delete` method to send a `DELETE` request to `/custom/:id`. The test passes if the response: +- Has a `200` status code. +- Has a `success` property in its data. +- The `success` property's value is true. -# Compensation Function +*** -In this chapter, you'll learn what a compensation function is and how to add it to a step. +## Pass Headers in Test Requests -## What is a Compensation Function +Some requests require passing headers. For example, all routes prefixed with `/store` must pass a publishable API key in the header. -A compensation function rolls back or undoes changes made by a step when an error occurs in the workflow. +The `get`, `post`, and `delete` methods accept an optional third parameter that you can pass a `headers` property to, whose value is an object of headers to pass in the request. -For example, if a step creates a record, the compensation function deletes the record when an error occurs later in the workflow. +### Pass Publishable API Key -By using compensation functions, you provide a mechanism that guarantees data consistency in your application and across systems. +For example, to pass a publishable API key in the header for a request to a `/store` route: -*** +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={headersHighlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { ApiKeyDTO } from "@medusajs/framework/types" +import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows" -## How to add a Compensation Function? +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + let pak: ApiKeyDTO + beforeAll(async () => { + pak = (await createApiKeysWorkflow(getContainer()).run({ + input: { + api_keys: [ + { + type: "publishable", + title: "Test Key", + created_by: "", + }, + ], + }, + })).result[0] + }) + describe("GET /custom", () => { + it("returns correct message", async () => { + const response = await api.get( + `/store/custom`, + { + headers: { + "x-publishable-api-key": pak.token, + }, + } + ) + + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("message") + expect(response.data.message).toEqual("Hello, World!") + }) + }) + }) + }, +}) -A compensation function is passed as a second parameter to the `createStep` function. +jest.setTimeout(60 * 1000) +``` -For example, create the file `src/workflows/hello-world.ts` with the following content: +In your test suit, you add a `beforeAll` hook to create a publishable API key before the tests run. To create the API key, you can use the `createApiKeysWorkflow` or the [API Key Module's service](https://docs.medusajs.com/resources/commerce-modules/api-key/index.html.md). -```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, in the test, you pass an object as the last parameter to `api.get` with a `headers` property. The `headers` property is an object with the key `x-publishable-api-key` and the value of the API key's token. -const step1 = createStep( - "step-1", - async () => { - const message = `Hello from step one!` +### Send Authenticated Requests - console.log(message) +If your custom route is accessible by authenticated users only, such as routes prefixed by `/admin` or `/store/customers/me`, you can create a test customer or user, generate a JWT token for them, and pass the token in the request's Authorization header. - return new StepResponse(message) - }, - async () => { - console.log("Oops! Rolling back my changes...") - } -) -``` +For example: -Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow. +- The `jsonwebtoken` is available in your application by default. +- For custom actor types, you only need to change the `actorType` value in the `jwt.sign` method. -*** +### Admin User -## Test the Compensation Function +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={adminHighlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import jwt from "jsonwebtoken" -Create a step in the same `src/workflows/hello-world.ts` file that throws an error: +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + describe("GET /custom", () => { + const headers: Record<string, string> = { + } + beforeEach(async () => { + const container = getContainer() + + const authModuleService = container.resolve("auth") + const userModuleService = container.resolve("user") + + const user = await userModuleService.createUsers({ + email: "admin@medusa.js", + + }) + const authIdentity = await authModuleService.createAuthIdentities({ + provider_identities: [ + { + provider: "emailpass", + entity_id: "admin@medusa.js", + provider_metadata: { + password: "supersecret", + }, + }, + ], + app_metadata: { + user_id: user.id, + }, + }) + + const token = jwt.sign( + { + actor_id: user.id, + actor_type: "user", + auth_identity_id: authIdentity.id, + }, + "supersecret", + { + expiresIn: "1d", + } + ) + + headers["authorization"] = `Bearer ${token}` + }) + it("returns correct message", async () => { + const response = await api.get( + `/admin/custom`, + { headers } + ) + + expect(response.status).toEqual(200) + }) + }) + }) + }, +}) -```ts title="src/workflows/hello-world.ts" -const step2 = createStep( - "step-2", - async () => { - throw new Error("Throwing an error...") - } -) +jest.setTimeout(60 * 1000) ``` -Then, create a workflow that uses the steps: +### Customer User -```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="integration-tests/http/custom-routes.spec.ts" highlights={customerHighlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { ApiKeyDTO } from "@medusajs/framework/types" +import jwt from "jsonwebtoken" +import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows" -// steps... +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + describe("GET /custom", () => { + const headers: Record<string, string> = { + } + beforeEach(async () => { + const container = getContainer() + + const authModuleService = container.resolve("auth") + const customerModuleService = container.resolve("customer") + + const customer = await customerModuleService.createCustomers({ + email: "admin@medusa.js", + + }) + const authIdentity = await authModuleService.createAuthIdentities({ + provider_identities: [ + { + provider: "emailpass", + entity_id: "customer@medusa.js", + provider_metadata: { + password: "supersecret", + }, + }, + ], + app_metadata: { + user_id: customer.id, + }, + }) + + const token = jwt.sign( + { + actor_id: customer.id, + actor_type: "customer", + auth_identity_id: authIdentity.id, + }, + "supersecret", + { + expiresIn: "1d", + } + ) + + headers["authorization"] = `Bearer ${token}` -const myWorkflow = createWorkflow( - "hello-world", - function (input) { - const str1 = step1() - step2() - return new WorkflowResponse({ - message: str1, - }) + const pak = (await createApiKeysWorkflow(getContainer()).run({ + input: { + api_keys: [ + { + type: "publishable", + title: "Test Key", + created_by: "", + }, + ], + }, + })).result[0] + + headers["x-publishable-api-key"] = pak.token + }) + it("returns correct message", async () => { + const response = await api.get( + `/store/customers/me/custom`, + { headers } + ) + + expect(response.status).toEqual(200) + }) + }) + }) + }, }) -export default myWorkflow +jest.setTimeout(60 * 1000) ``` -Finally, execute the workflow from an API route: +In the test suite, you add a `beforeEach` hook that creates a user or customer, an auth identity, and generates a JWT token for them. The JWT token is then set in the `Authorization` header of the request. -```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" +You also create and pass a publishable API key in the header for the customer as it's required for requests to `/store` routes. Learn more in [this section](#pass-publishable-api-key). -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await myWorkflow(req.scope) - .run() +*** - res.send(result) -} -``` +## Upload Files in Test Requests -Run the Medusa application and send a `GET` request to `/workflow`: +If your API route requires uploading a file, create a `FormData` object imported from the `form-data` object, then pass the form data headers in the request. -```bash -curl http://localhost:9000/workflow -``` +For example: -In the console, you'll see: +The `form-data` package is available by default. -- `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. +```ts title="integration-tests/http/custom-routes.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import FormData from "form-data" -*** +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + describe("GET /custom", () => { + it("upload file", async () => { + const form = new FormData() + form.append("files", Buffer.from("content 1"), "image1.jpg") + form.append("files", Buffer.from("content 2"), "image2.jpg") -## Pass Input to Compensation Function + const response = await api.post(`/custom`, form, { + headers: form.getHeaders(), + }) + + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("files") + expect(response.data.files).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + url: expect.any(String), + }), + ]) + ) + }) + }) + }) + }, +}) -If a step creates a record, the compensation function must receive the ID of the record to remove it. +jest.setTimeout(60 * 1000) +``` -To pass input to the compensation function, pass a second parameter in the `StepResponse` returned by the step. +You don't have to actually upload a file, you use the `form.append` method to append to a `files` field in the form data object, and you pass random content using the `Buffer.from` method. -For example: +Then, you pass to the `api.post` method the form data object as a second parameter, and an object with the `headers` property set to the form data object's headers as a third parameter. -```ts highlights={inputHighlights} -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" +If you're passing authentication or other headers, you can pass both the form data headers and the authentication headers in the same object: -const step1 = createStep( - "step-1", - async () => { - return new StepResponse( - `Hello from step one!`, - { message: "Oops! Rolling back my changes..." } - ) +```ts title="integration-tests/http/custom-routes.spec.ts" +const response = await api.post(`/custom`, form, { + headers: { + ...form.getHeaders(), + ...authHeaders, }, - async ({ message }) => { - console.log(message) - } -) +}) ``` -In this example, the step passes an object as a second parameter to `StepResponse`. -The compensation function receives the object and uses its `message` property to log a message. +# Example: Write Integration Tests for Workflows -*** +In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framwork. -## Resolve Resources from the Medusa Container +### Prerequisites -The compensation function receives an object second parameter. The object has a `container` property that you use to resolve resources from the Medusa container. +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -For example: +## Write Integration Test for Workflow -```ts -import { +Consider you have the following workflow defined at `src/workflows/hello-world.ts`: + +```ts title="src/workflows/hello-world.ts" +import { + createWorkflow, createStep, StepResponse, + WorkflowResponse, } from "@medusajs/framework/workflows-sdk" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -const step1 = createStep( - "step-1", - async () => { - return new StepResponse( - `Hello from step one!`, - { message: "Oops! Rolling back my changes..." } - ) - }, - async ({ message }, { container }) => { +const step1 = createStep("step-1", () => { + return new StepResponse("Hello, World!") +}) + +export const helloWorldWorkflow = createWorkflow( + "hello-world-workflow", + () => { + const message = step1() + + return new WorkflowResponse(message) + } +) +``` + +To write a test for this workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content: + +```ts title="integration-tests/http/workflow.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { helloWorldWorkflow } from "../../src/workflows/hello-world" + +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test hello-world workflow", () => { + it("returns message", async () => { + const { result } = await helloWorldWorkflow(getContainer()) + .run() + + expect(result).toEqual("Hello, World!") + }) + }) + }, +}) + +jest.setTimeout(60 * 1000) +``` + +You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`. + +### 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/custom-routes.spec.ts" +// in your test's file +jest.setTimeout(60 * 1000) +``` + +*** + +## Run Test + +Run the following command to run your tests: + +```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 `integrations/http` directory. + +*** + +## Test That a Workflow Throws an Error + +You might want to test that a workflow throws an error in certain cases. To test this: + +- Disable the `throwOnError` option when executing the workflow. +- Use the returned `errors` property to check what errors were thrown. + +For example, if you have a step that throws this error: + +```ts title="src/workflows/hello-world.ts" +import { MedusaError } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" + +const step1 = createStep("step-1", () => { + throw new MedusaError(MedusaError.Types.NOT_FOUND, "Item doesn't exist") +}) +``` + +You can write the following test to ensure that the workflow throws that error: + +```ts title="integration-tests/http/workflow.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { helloWorldWorkflow } from "../../src/workflows/hello-world" + +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test hello-world workflow", () => { + it("returns message", async () => { + const { errors } = await helloWorldWorkflow(getContainer()) + .run({ + throwOnError: false, + }) + + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].error.message).toBe("Item doesn't exist") + }) + }) + }, +}) + +jest.setTimeout(60 * 1000) +``` + +The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, being the error thrown. + +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<HelloModuleService>({ + moduleName: HELLO_MODULE, + moduleModels: [MyCustom], + resolve: "./src/modules/hello", + testSuite: ({ service }) => { + describe("HelloModuleService", () => { + it("says hello world", () => { + const message = service.getMessage() + + expect(message).toEqual("Hello, World!") + }) + }) + }, +}) + +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. + + +# 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? + +By default, when an error occurs in a workflow, it throws that error, and the execution stops. + +You can configure the workflow to return the errors instead so that you can access and handle them differently. + +For example: + +```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import myWorkflow from "../../../workflows/hello-world" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result, errors } = await myWorkflow(req.scope) + .run({ + // ... + throwOnError: false, + }) + + if (errors.length) { + return res.send({ + errors: errors.map((error) => error.error), + }) + } + + res.send(result) +} + +``` + +The object passed to the `run` method accepts a `throwOnError` property. When disabled, the errors are returned in the `errors` property of `run`'s output. + +The value of `errors` is an array of error objects. Each object has an `error` property, whose value is the name or text of the thrown error. + + +# Expose a Workflow Hook + +In this chapter, you'll learn how to expose a hook in your workflow. + +## When to Expose a Hook + +Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow. + +Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead. + +*** + +## 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 { + createStep, + createHook, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { createProductStep } from "./steps/create-product" + +export const myWorkflow = createWorkflow( + "my-workflow", + function (input) { + const product = createProductStep(input) + const productCreatedHook = createHook( + "productCreated", + { productId: product.id } + ) + + return new WorkflowResponse(product, { + hooks: [productCreatedHook], + }) + } +) +``` + +The `createHook` function accepts two parameters: + +1. The first is a string indicating the hook's name. You use this to consume the hook later. +2. The second is the input to pass to the hook handler. + +The workflow must also pass an object having a `hooks` property as a second parameter to the `WorkflowResponse` constructor. Its value is an array of the workflow's hooks. + +### How to Consume the Hook? + +To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content: + +```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights} +import { myWorkflow } from "../my-workflow" + +myWorkflow.hooks.productCreated( + async ({ productId }, { container }) => { + // TODO perform an action + } +) +``` + +The hook is available on the workflow's `hooks` property using its name `productCreated`. + +You invoke the hook, passing a step function (the hook handler) as a parameter. + + +# Compensation Function + +In this chapter, you'll learn what a compensation function is and how to add it to a step. + +## What is a Compensation Function + +A compensation function rolls back or undoes changes made by a step when an error occurs in the workflow. + +For example, if a step creates a record, the compensation function deletes the record when an error occurs later in the workflow. + +By using compensation functions, you provide a mechanism that guarantees data consistency in your application and across systems. + +*** + +## How to add a Compensation Function? + +A compensation function is passed as a second parameter to the `createStep` function. + +For example, create the file `src/workflows/hello-world.ts` with the following content: + +```ts title="src/workflows/hello-world.ts" highlights={[["15"], ["16"], ["17"]]} collapsibleLines="1-5" expandButtonLabel="Show Imports" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + const message = `Hello from step one!` + + console.log(message) + + return new StepResponse(message) + }, + async () => { + console.log("Oops! Rolling back my changes...") + } +) +``` + +Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow. + +*** + +## Test the Compensation Function + +Create a step in the same `src/workflows/hello-world.ts` file that throws an error: + +```ts title="src/workflows/hello-world.ts" +const step2 = createStep( + "step-2", + async () => { + throw new Error("Throwing an error...") + } +) +``` + +Then, create a workflow that uses the steps: + +```ts title="src/workflows/hello-world.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +// other imports... + +// steps... + +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str1 = step1() + step2() + + return new WorkflowResponse({ + message: str1, + }) +}) + +export default myWorkflow +``` + +Finally, execute the workflow from an API route: + +```ts title="src/api/workflow/route.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import myWorkflow from "../../../workflows/hello-world" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await myWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +Run the Medusa application and send a `GET` request to `/workflow`: + +```bash +curl http://localhost:9000/workflow +``` + +In the console, you'll see: + +- `Hello from step one!` logged in the terminal, indicating that the first step ran successfully. +- `Oops! Rolling back my changes...` logged in the terminal, indicating that the second step failed and the compensation function of the first step ran consequently. + +*** + +## Pass Input to Compensation Function + +If a step creates a record, the compensation function must receive the ID of the record to remove it. + +To pass input to the compensation function, pass a second parameter in the `StepResponse` returned by the step. + +For example: + +```ts highlights={inputHighlights} +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + return new StepResponse( + `Hello from step one!`, + { message: "Oops! Rolling back my changes..." } + ) + }, + async ({ message }) => { + console.log(message) + } +) +``` + +In this example, the step passes an object as a second parameter to `StepResponse`. + +The compensation function receives the object and uses its `message` property to log a message. + +*** + +## Resolve Resources from the Medusa Container + +The compensation function receives an object second parameter. The object has a `container` property that you use to resolve resources from the Medusa container. + +For example: + +```ts +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +const step1 = createStep( + "step-1", + async () => { + return new StepResponse( + `Hello from step one!`, + { message: "Oops! Rolling back my changes..." } + ) + }, + async ({ message }, { container }) => { const logger = container.resolve( ContainerRegistrationKeys.LOGGER ) @@ -12382,157 +13151,27 @@ Since `then` returns a value different than the step's result, you pass to the ` The second and third parameters are the same as the parameters you previously passed to `when`. -# Execute Another Workflow - -In this chapter, you'll learn how to execute a workflow in another. - -## Execute in a Workflow +# Workflow Constraints -To execute a workflow in another, use the `runAsStep` method that every workflow has. +This chapter lists constraints of defining a workflow or its steps. -For example: +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. -```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports" -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" +This creates restrictions related to variable manipulations, using if-conditions, and other constraints. This chapter lists these constraints and provides their alternatives. -const workflow = createWorkflow( - "hello-world", - async (input) => { - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - // ... - ], - }, - }) +## Workflow Constraints - // ... - } -) -``` +### No Async Functions -Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. +The function passed to `createWorkflow` can’t be an async function: -The object has an `input` property to pass input to the workflow. - -*** - -## Preparing Input Data - -If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. - -Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). - -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 -} - -const workflow = createWorkflow( - "hello-product", - async (input: WorkflowInput) => { - const createProductsData = transform({ - input, - }, (data) => [ - { - title: `Hello ${data.input.title}`, - }, - ]) - - const products = createProductsWorkflow.runAsStep({ - input: { - products: createProductsData, - }, - }) - - // ... - } -) -``` - -In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as an input to the `createProductsWorkflow`. - -*** - -## Run Workflow Conditionally - -To run a workflow in another based on a condition, use when-then from the Workflows SDK. - -Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). - -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. - - -# Workflow Constraints - -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. - -This creates restrictions related to variable manipulations, using if-conditions, and other constraints. This chapter lists these constraints and provides their alternatives. - -## Workflow Constraints - -### No Async Functions - -The function passed to `createWorkflow` can’t be an async function: - -```ts highlights={[["4", "async", "Function can't be async."], ["11", "", "Correct way of defining the function."]]} -// Don't -const myWorkflow = createWorkflow( - "hello-world", - async function (input: WorkflowInput) { - // ... -}) +```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( @@ -13148,6 +13787,136 @@ To find a full example of a long-running workflow, refer to the [restaurant-deli 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. +# Execute Another Workflow + +In this chapter, you'll learn how to execute a workflow in another. + +## Execute in a Workflow + +To execute a workflow in another, use the `runAsStep` method that every workflow has. + +For example: + +```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports" +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +const workflow = createWorkflow( + "hello-world", + async (input) => { + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + // ... + ], + }, + }) + + // ... + } +) +``` + +Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. + +The object has an `input` property to pass input to the workflow. + +*** + +## Preparing Input Data + +If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. + +Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). + +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 +} + +const workflow = createWorkflow( + "hello-product", + async (input: WorkflowInput) => { + const createProductsData = transform({ + input, + }, (data) => [ + { + title: `Hello ${data.input.title}`, + }, + ]) + + const products = createProductsWorkflow.runAsStep({ + input: { + products: createProductsData, + }, + }) + + // ... + } +) +``` + +In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as an input to the `createProductsWorkflow`. + +*** + +## Run Workflow Conditionally + +To run a workflow in another based on a condition, use when-then from the Workflows SDK. + +Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). + +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. + + # Multiple Step Usage in Workflow In this chapter, you'll learn how to use a step multiple times in a workflow. @@ -13222,6 +13991,60 @@ The `config` method accepts an object with a `name` property. Its value is a new The first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`. +# Run Workflow Steps in Parallel + +In this chapter, you’ll learn how to run workflow steps in parallel. + +## parallelize Utility Function + +If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK. + +The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step. + +For example: + +```ts highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" +import { + createWorkflow, + WorkflowResponse, + parallelize, +} from "@medusajs/framework/workflows-sdk" +import { + createProductStep, + getProductStep, + createPricesStep, + attachProductToSalesChannelStep, +} from "./steps" + +interface WorkflowInput { + title: string +} + +const myWorkflow = createWorkflow( + "my-workflow", + (input: WorkflowInput) => { + const product = createProductStep(input) + + const [prices, productSalesChannel] = parallelize( + createPricesStep(product), + attachProductToSalesChannelStep(product) + ) + + const id = product.id + const refetchedProduct = getProductStep(product.id) + + return new WorkflowResponse(refetchedProduct) + } +) +``` + +The `parallelize` function accepts the steps to run in parallel as a parameter. + +It returns an array of the steps' results in the same order they're passed to the `parallelize` function. + +So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`. + + # Retry Failed Steps In this chapter, you’ll learn how to configure steps to allow retrial on failure. @@ -13308,60 +14131,6 @@ By setting `retryInterval` on a step, a workflow becomes a [long-running workflo 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). -# Run Workflow Steps in Parallel - -In this chapter, you’ll learn how to run workflow steps in parallel. - -## parallelize Utility Function - -If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK. - -The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step. - -For example: - -```ts highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" -import { - createWorkflow, - WorkflowResponse, - parallelize, -} from "@medusajs/framework/workflows-sdk" -import { - createProductStep, - getProductStep, - createPricesStep, - attachProductToSalesChannelStep, -} from "./steps" - -interface WorkflowInput { - title: string -} - -const myWorkflow = createWorkflow( - "my-workflow", - (input: WorkflowInput) => { - const product = createProductStep(input) - - const [prices, productSalesChannel] = parallelize( - createPricesStep(product), - attachProductToSalesChannelStep(product) - ) - - const id = product.id - const refetchedProduct = getProductStep(product.id) - - return new WorkflowResponse(refetchedProduct) - } -) -``` - -The `parallelize` function accepts the steps to run in parallel as a parameter. - -It returns an array of the steps' results in the same order they're passed to the `parallelize` function. - -So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`. - - # Store Workflow Executions In this chapter, you'll learn how to store workflow executions in the database and access them later. @@ -13507,216 +14276,6 @@ if (workflowExecution.state === "failed") { Other state values include `done`, `invoking`, and `compensating`. -# Workflow Timeout - -In this chapter, you’ll learn how to set a timeout for workflows and steps. - -## What is a Workflow Timeout? - -By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. - -You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown. - -### Timeout Doesn't Stop Step Execution - -Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result. - -*** - -## Configure Workflow Timeout - -The `createWorkflow` function can accept a configuration object instead of the workflow’s name. - -In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. - -For example: - -```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" -import { - createStep, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - "step-1", - async () => { - // ... - } -) - -const myWorkflow = createWorkflow({ - name: "hello-world", - timeout: 2, // 2 seconds -}, function () { - const str1 = step1() - - return new WorkflowResponse({ - message: str1, - }) -}) - -export default myWorkflow - -``` - -This workflow's executions fail if they run longer than two seconds. - -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`. - -*** - -## Configure Step Timeout - -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. - -The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. - -For example: - -```tsx -const step1 = createStep( - { - name: "step-1", - timeout: 2, // 2 seconds - }, - async () => { - // ... - } -) -``` - -This step's executions fail if they run longer than two seconds. - -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`. - - -# Workflow Hooks - -In this chapter, you'll learn what a workflow hook is and how to consume them. - -## What is a Workflow Hook? - -A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler. - -Medusa exposes hooks in many of its workflows that are used in its API routes. You can consume those hooks to add your custom logic. - -Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks. - -You want to perform a custom action during a workflow's execution, such as when a product is created. - -*** - -## How to Consume a Hook? - -A workflow has a special `hooks` property which is an object that holds its hooks. - -So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory: - -- Import the workflow. -- Access its hook using the `hooks` property. -- Pass the hook a step function as a parameter to consume it. - -For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content: - -```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights} -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" - -createProductsWorkflow.hooks.productsCreated( - async ({ products }, { container }) => { - // TODO perform an action - } -) -``` - -The `productsCreated` hook is available on the workflow's `hooks` property by its name. - -You invoke the hook, passing a step function (the hook handler) as a parameter. - -Now, when a product is created using the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), your hook handler is executed after the product is created. - -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. - -### Hook Handler Parameter - -Since a hook handler is essentially a step function, it receives the hook's input as a first parameter, and an object holding a `container` property as a second parameter. - -Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product. - -### Hook Handler Compensation - -Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook. - -For example: - -```ts title="src/workflows/hooks/product-created.ts" -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" - -createProductsWorkflow.hooks.productsCreated( - async ({ products }, { container }) => { - // TODO perform an action - - return new StepResponse(undefined, { ids }) - }, - async ({ ids }, { container }) => { - // undo the performed action - } -) -``` - -The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler. - -The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function. - -It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container. - -### Additional Data Property - -Medusa's workflows pass in the hook's input an `additional_data` property: - -```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]} -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" - -createProductsWorkflow.hooks.productsCreated( - async ({ products, additional_data }, { container }) => { - // TODO perform an action - } -) -``` - -This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow. - -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 - -You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow: - -```ts title="src/workflows/hooks/product-created.ts" highlights={[["10", "additional_data"]]} -import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" - -export async function POST(req: MedusaRequest, res: MedusaResponse) { - await createProductsWorkflow(req.scope).run({ - input: { - products: [ - // ... - ], - additional_data: { - custom_field: "test", - }, - }, - }) -} -``` - -Your hook handler then receives that passed data in the `additional_data` object. - - # 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. @@ -13922,814 +14481,675 @@ const myWorkflow = createWorkflow( ``` -# Example: Integration Tests for a Module +# Workflow Hooks -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 what a workflow hook is and how to consume them. -### Prerequisites +## What is a Workflow Hook? -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) +A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler. -## Write Integration Test for Module +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. -Consider a `hello` module with a `HelloModuleService` that has a `getMessage` method: +Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks. -```ts title="src/modules/hello/service.ts" -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" +You want to perform a custom action during a workflow's execution, such as when a product is created. -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - getMessage(): string { - return "Hello, World!" - } -} +*** -export default HelloModuleService -``` +## How to Consume a Hook? -To create an integration test for the method, create the file `src/modules/hello/__tests__/service.spec.ts` with the following content: +A workflow has a special `hooks` property which is an object that holds its hooks. -```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" +So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory: -moduleIntegrationTestRunner<HelloModuleService>({ - moduleName: HELLO_MODULE, - moduleModels: [MyCustom], - resolve: "./src/modules/hello", - testSuite: ({ service }) => { - describe("HelloModuleService", () => { - it("says hello world", () => { - const message = service.getMessage() +- Import the workflow. +- Access its hook using the `hooks` property. +- Pass the hook a step function as a parameter to consume it. - expect(message).toEqual("Hello, World!") - }) - }) - }, -}) +For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content: -jest.setTimeout(60 * 1000) +```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +createProductsWorkflow.hooks.productsCreated( + async ({ products }, { container }) => { + // TODO perform an action + } +) ``` -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 `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. -## Run Test +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. -Run the following command to run your module integration tests: +A hook can have only one handler. -```bash npm2yarn -npm run test:integration:modules -``` +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. -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). +### Hook Handler Parameter -This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. +Since a hook handler is essentially a step function, it receives the hook's input as a first parameter, and an object holding a `container` property as a second parameter. +Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product. -# Example: Write Integration Tests for Workflows +### Hook Handler Compensation -In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framwork. +Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook. -### Prerequisites +For example: -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) +```ts title="src/workflows/hooks/product-created.ts" +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" -## Write Integration Test for Workflow +createProductsWorkflow.hooks.productsCreated( + async ({ products }, { container }) => { + // TODO perform an action -Consider you have the following workflow defined at `src/workflows/hello-world.ts`: + return new StepResponse(undefined, { ids }) + }, + async ({ ids }, { container }) => { + // undo the performed action + } +) +``` -```ts title="src/workflows/hello-world.ts" -import { - createWorkflow, - createStep, - StepResponse, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler. -const step1 = createStep("step-1", () => { - return new StepResponse("Hello, World!") -}) +The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function. -export const helloWorldWorkflow = createWorkflow( - "hello-world-workflow", - () => { - const message = step1() +It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container. - return new WorkflowResponse(message) +### Additional Data Property + +Medusa's workflows pass in the hook's input an `additional_data` property: + +```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +createProductsWorkflow.hooks.productsCreated( + async ({ products, additional_data }, { container }) => { + // TODO perform an action } ) ``` -To write a test for this workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content: +This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow. -```ts title="integration-tests/http/workflow.spec.ts" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { helloWorldWorkflow } from "../../src/workflows/hello-world" +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). -medusaIntegrationTestRunner({ - testSuite: ({ getContainer }) => { - describe("Test hello-world workflow", () => { - it("returns message", async () => { - const { result } = await helloWorldWorkflow(getContainer()) - .run() +### Pass Additional Data to Workflow - expect(result).toEqual("Hello, World!") - }) - }) - }, -}) +You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow: -jest.setTimeout(60 * 1000) -``` +```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" -You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`. +export async function POST(req: MedusaRequest, res: MedusaResponse) { + await createProductsWorkflow(req.scope).run({ + input: { + products: [ + // ... + ], + additional_data: { + custom_field: "test", + }, + }, + }) +} +``` -### Jest Timeout +Your hook handler then receives that passed data in the `additional_data` object. -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/custom-routes.spec.ts" -// in your test's file -jest.setTimeout(60 * 1000) -``` +# Workflow Timeout -*** +In this chapter, you’ll learn how to set a timeout for workflows and steps. -## Run Test +## What is a Workflow Timeout? -Run the following command to run your tests: +By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. -```bash npm2yarn -npm run test:integration -``` +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. -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). +### Timeout Doesn't Stop Step Execution -This runs your Medusa application and runs the tests available under the `integrations/http` directory. +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. *** -## Test That a Workflow Throws an Error - -You might want to test that a workflow throws an error in certain cases. To test this: - -- Disable the `throwOnError` option when executing the workflow. -- Use the returned `errors` property to check what errors were thrown. +## Configure Workflow Timeout -For example, if you have a step that throws this error: +The `createWorkflow` function can accept a configuration object instead of the workflow’s name. -```ts title="src/workflows/hello-world.ts" -import { MedusaError } from "@medusajs/framework/utils" -import { createStep } from "@medusajs/framework/workflows-sdk" +In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. -const step1 = createStep("step-1", () => { - throw new MedusaError(MedusaError.Types.NOT_FOUND, "Item doesn't exist") -}) -``` +For example: -You can write the following test to ensure that the workflow throws that error: +```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" +import { + createStep, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -```ts title="integration-tests/http/workflow.spec.ts" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { helloWorldWorkflow } from "../../src/workflows/hello-world" +const step1 = createStep( + "step-1", + async () => { + // ... + } +) -medusaIntegrationTestRunner({ - testSuite: ({ getContainer }) => { - describe("Test hello-world workflow", () => { - it("returns message", async () => { - const { errors } = await helloWorldWorkflow(getContainer()) - .run({ - throwOnError: false, - }) +const myWorkflow = createWorkflow({ + name: "hello-world", + timeout: 2, // 2 seconds +}, function () { + const str1 = step1() - expect(errors.length).toBeGreaterThan(0) - expect(errors[0].error.message).toBe("Item doesn't exist") - }) - }) - }, + return new WorkflowResponse({ + message: str1, + }) }) -jest.setTimeout(60 * 1000) +export default myWorkflow + ``` -The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, being the error thrown. +This workflow's executions fail if they run longer than two seconds. -If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. +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`. +*** -# Example: Write Integration Tests for API Routes +## Configure Step Timeout -In this chapter, you'll learn how to write integration tests for API routes using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framework. +Alternatively, you can configure the timeout for a step rather than the entire workflow. -### Prerequisites +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. -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) +The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. -## Test a GET API Route +For example: -Consider the following API route created at `src/api/custom/route.ts`: +```tsx +const step1 = createStep( + { + name: "step-1", + timeout: 2, // 2 seconds + }, + async () => { + // ... + } +) +``` -```ts title="src/api/custom/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +This step's executions fail if they run longer than two seconds. -export async function GET( - req: MedusaRequest, - res: MedusaResponse -){ - res.json({ - message: "Hello, World!", - }) -} -``` +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`. -To write an integration test that tests this API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content: -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={getHighlights} -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +# Commerce Modules -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - describe("GET /custom", () => { - it("returns correct message", async () => { - const response = await api.get( - `/custom` - ) - - expect(response.status).toEqual(200) - expect(response.data).toHaveProperty("message") - expect(response.data.message).toEqual("Hello, World!") - }) - }) - }) - }, -}) +In this section of the documentation, you'll find guides and references related to Medusa's commerce modules. -jest.setTimeout(60 * 1000) -``` +A commerce module provides features for a commerce domain within its service. The Medusa application exposes these features in its API routes to clients. -You use the `medusaIntegrationTestRunner` to write your tests. +A commerce module also defines data models, representing tables in the database. Medusa's framework and tools allow you to extend these data models to add custom fields. -You add a single test that sends a `GET` request to `/custom` using the `api.get` method. For the test to pass, the response is expected to: +## Commerce Modules List -- Have a code status `200`, -- Have a `message` property in the returned data. -- Have the value of the `message` property equal to `Hello, World!`. +*** -### Run Tests +## How to Use Modules -Run the following command to run your tests: +The Commerce Modules can be used in many use cases, including: -```bash npm2yarn -npm run test:integration -``` +- Medusa Application: The Medusa application uses the Commerce Modules to expose commerce features through the REST APIs. +- Serverless Application: Use the Commerce Modules in a serverless application, such as a Next.js application, without having to manage a fully-fledged ecommerce system. You can use it by installing it in your Node.js project as an NPM package. +- Node.js Application: Use the Commerce Modules in any Node.js application by installing it with NPM. -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. +# API Key Module -### Jest Timeout +In this section of the documentation, you will find resources to learn more about the API Key Module and how to use it in your application. -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: +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/developer/index.html.md) to learn how to manage publishable and secret API keys using the dashboard. -```ts title="integration-tests/http/custom-routes.spec.ts" -// in your test's file -jest.setTimeout(60 * 1000) -``` +Medusa has API-key related features available out-of-the-box through the API Key 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 API Key Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## API Key Features + +- [API Key Types and Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts/index.html.md): Manage API keys in your store. You can create both publishable and secret API keys for different use cases. +- [Token Verification](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#token-verification/index.html.md): Verify tokens of secret API keys to authenticate users or actions. +- [Revoke Keys](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#api-key-expiration/index.html.md): Revoke keys to disable their use permanently. +- Roll API Keys: Roll API keys by [revoking](https://docs.medusajs.com/references/api-key/revoke/index.html.md) a key then [re-creating it](https://docs.medusajs.com/references/api-key/createApiKeys/index.html.md). *** -## Test a POST API Route +## How to Use the API Key Module -Suppose you have a `hello` module whose main service extends the service factory, and that has the following model: +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. -```ts title="src/modules/hello/models/my-custom.ts" -import { model } from "@medusajs/framework/utils" +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. -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), -}) +For example: -export default MyCustom +```ts title="src/workflows/create-api-key.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createApiKeyStep = createStep( + "create-api-key", + async ({}, { container }) => { + const apiKeyModuleService = container.resolve(Modules.API_KEY) + + const apiKey = await apiKeyModuleService.createApiKeys({ + title: "Publishable API key", + type: "publishable", + created_by: "user_123", + }) + + return new StepResponse({ apiKey }, apiKey.id) + }, + async (apiKeyId, { container }) => { + const apiKeyModuleService = container.resolve(Modules.API_KEY) + + await apiKeyModuleService.deleteApiKeys([apiKeyId]) + } +) + +export const createApiKeyWorkflow = createWorkflow( + "create-api-key", + () => { + const { apiKey } = createApiKeyStep() + + return new WorkflowResponse({ + apiKey, + }) + } +) ``` -And consider that the file `src/api/custom/route.ts` defines another route handler for `POST` requests: +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -```ts title="src/api/custom/route.ts" -// other imports... -import HelloModuleService from "../../../modules/hello/service" +### 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 { createApiKeyWorkflow } from "../../workflows/create-api-key" -export async function POST( +export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const helloModuleService: HelloModuleService = req.scope.resolve( - "helloModuleService" - ) - - const myCustom = await helloModuleService.createMyCustoms( - req.body - ) + const { result } = await createApiKeyWorkflow(req.scope) + .run() - res.json({ - my_custom: myCustom, - }) + res.send(result) } ``` -This API route creates a new record of `MyCustom`. +### Subscriber -To write tests for this API route, add the following at the end of the `testSuite` function in `integration-tests/http/custom-routes.spec.ts`: +```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 { createApiKeyWorkflow } from "../workflows/create-api-key" -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={postHighlights} -// other imports... -import HelloModuleService from "../../src/modules/hello/service" +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createApiKeyWorkflow(container) + .run() -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - // other tests... + console.log(result) +} - describe("POST /custom", () => { - const id = "1" +export const config: SubscriberConfig = { + event: "user.created", +} +``` - it("Creates my custom", async () => { - - const response = await api.post( - `/custom`, - { - id, - name: "Test", - } - ) - - expect(response.status).toEqual(200) - expect(response.data).toHaveProperty("my_custom") - expect(response.data.my_custom).toEqual({ - id, - name: "Test", - created_at: expect.any(String), - updated_at: expect.any(String), - }) - }) - }) - }) - }, -}) +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createApiKeyWorkflow } from "../workflows/create-api-key" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createApiKeyWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} ``` -This adds a test for the `POST /custom` API route. It uses `api.post` to send the POST request. The `api.post` method accepts as a second parameter the data to pass in the request body. +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -The test passes if the response has: +*** -- Status code `200`. -- A `my_custom` property in its data. -- Its `id` and `name` match the ones provided to the request. -### Tear Down Created Record +# Cart Module -To ensure consistency in the database for the rest of the tests after the above test is executed, utilize [Jest's setup and teardown hooks](https://jestjs.io/docs/setup-teardown) to delete the created record. +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. -Use the `getContainer` function passed as a parameter to the `testSuite` function to resolve a service and use it for setup or teardown purposes +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. -So, add an `afterAll` hook in the `describe` block for `POST /custom`: +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -```ts title="integration-tests/http/custom-routes.spec.ts" -// other imports... -import HelloModuleService from "../../src/modules/hello/service" +## Cart Features -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - // other tests... +- [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. - describe("POST /custom", () => { - // ... - afterAll(() => async () => { - const helloModuleService: HelloModuleService = getContainer().resolve( - "helloModuleService" - ) +*** - await helloModuleService.deleteMyCustoms(id) - }) - }) +## 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. + +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-cart.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createCartStep = createStep( + "create-cart", + 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, + }, + ], }) + + return new StepResponse({ cart }, cart.id) }, -}) -``` + async (cartId, { container }) => { + if (!cartId) { + return + } + const cartModuleService = container.resolve(Modules.CART) -The `afterAll` hook resolves the `HelloModuleService` and use its `deleteMyCustoms` to delete the record created by the test. + await cartModuleService.deleteCarts([cartId]) + } +) -*** +export const createCartWorkflow = createWorkflow( + "create-cart", + () => { + const { cart } = createCartStep() -## Test a DELETE API Route + return new WorkflowResponse({ + cart, + }) + } +) +``` -Consider a `/custom/:id` API route created at `src/api/custom/[id]/route.ts`: +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -```ts title="src/api/custom/[id]/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import HelloModuleService from "../../../modules/hello/service" +### API Route -export async function DELETE( +```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 { createCartWorkflow } from "../../workflows/create-cart" + +export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const helloModuleService: HelloModuleService = req.scope.resolve( - "helloModuleService" - ) - - await helloModuleService.deleteMyCustoms(req.params.id) + const { result } = await createCartWorkflow(req.scope) + .run() - res.json({ - success: true, - }) + res.send(result) } ``` -This API route accepts an ID path parameter, and uses the `HelloModuleService` to delete a `MyCustom` record by that ID. +### Subscriber -To add tests for this API route, add the following to `integration-tests/http/custom-routes.spec.ts`: +```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" -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={deleteHighlights} -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - // ... +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createCartWorkflow(container) + .run() - describe("DELETE /custom/:id", () => { - const id = "1" + console.log(result) +} - beforeAll(() => async () => { - const helloModuleService: HelloModuleService = getContainer().resolve( - "helloModuleService" - ) +export const config: SubscriberConfig = { + event: "user.created", +} +``` - await helloModuleService.createMyCustoms({ - id, - name: "Test", - }) - }) +### Scheduled Job - it("Deletes my custom", async () => { - const response = await api.delete( - `/custom/${id}` - ) +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createCartWorkflow } from "../workflows/create-cart" - expect(response.status).toEqual(200) - expect(response.data).toHaveProperty("success") - expect(response.data.success).toBeTruthy() - }) - }) - }) - }, -}) -``` +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createCartWorkflow(container) + .run() -This adds a new test for the `DELETE /custom/:id` API route. You use the `beforeAll` hook to create a `MyCustom` record using the `HelloModuleService`. + console.log(result) +} -In the test, you use the `api.delete` method to send a `DELETE` request to `/custom/:id`. The test passes if the response: +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` -- Has a `200` status code. -- Has a `success` property in its data. -- The `success` property's value is true. +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). *** -## Pass Headers in Test Requests -Some requests require passing headers. For example, all routes prefixed with `/store` must pass a publishable API key in the header. +# Auth Module -The `get`, `post`, and `delete` methods accept an optional third parameter that you can pass a `headers` property to, whose value is an object of headers to pass in the request. +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. -### Pass Publishable API Key +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. -For example, to pass a publishable API key in the header for a request to a `/store` route: +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={headersHighlights} -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { ApiKeyDTO } from "@medusajs/framework/types" -import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows" +## Auth Features -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - let pak: ApiKeyDTO - beforeAll(async () => { - pak = (await createApiKeysWorkflow(getContainer()).run({ - input: { - api_keys: [ - { - type: "publishable", - title: "Test Key", - created_by: "", - }, - ], - }, - })).result[0] - }) - describe("GET /custom", () => { - it("returns correct message", async () => { - const response = await api.get( - `/store/custom`, - { - headers: { - "x-publishable-api-key": pak.token, - }, - } - ) - - expect(response.status).toEqual(200) - expect(response.data).toHaveProperty("message") - expect(response.data.message).toEqual("Hello, World!") - }) - }) - }) - }, -}) +- [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. -jest.setTimeout(60 * 1000) -``` +*** -In your test suit, you add a `beforeAll` hook to create a publishable API key before the tests run. To create the API key, you can use the `createApiKeysWorkflow` or the [API Key Module's service](https://docs.medusajs.com/resources/commerce-modules/api-key/index.html.md). +## How to Use the Auth Module -Then, in the test, you pass an object as the last parameter to `api.get` with a `headers` property. The `headers` property is an object with the key `x-publishable-api-key` and the value of the API key's token. +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. -### Send Authenticated Requests - -If your custom route is accessible by authenticated users only, such as routes prefixed by `/admin` or `/store/customers/me`, you can create a test customer or user, generate a JWT token for them, and pass the token in the request's Authorization header. +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: -- The `jsonwebtoken` is available in your application by default. -- For custom actor types, you only need to change the `actorType` value in the `jwt.sign` method. - -### Admin User - -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={adminHighlights} -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import jwt from "jsonwebtoken" - -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - describe("GET /custom", () => { - const headers: Record<string, string> = { - } - beforeEach(async () => { - const container = getContainer() - - const authModuleService = container.resolve("auth") - const userModuleService = container.resolve("user") - - const user = await userModuleService.createUsers({ - email: "admin@medusa.js", - - }) - const authIdentity = await authModuleService.createAuthIdentities({ - provider_identities: [ - { - provider: "emailpass", - entity_id: "admin@medusa.js", - provider_metadata: { - password: "supersecret", - }, - }, - ], - app_metadata: { - user_id: user.id, - }, - }) - - const token = jwt.sign( - { - actor_id: user.id, - actor_type: "user", - auth_identity_id: authIdentity.id, - }, - "supersecret", - { - expiresIn: "1d", - } - ) - - headers["authorization"] = `Bearer ${token}` - }) - it("returns correct message", async () => { - const response = await api.get( - `/admin/custom`, - { headers } - ) - - expect(response.status).toEqual(200) - }) - }) - }) - }, -}) - -jest.setTimeout(60 * 1000) -``` - -### Customer User +```ts title="src/workflows/authenticate-user.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" -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={customerHighlights} -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { ApiKeyDTO } from "@medusajs/framework/types" -import jwt from "jsonwebtoken" -import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows" +type Input = { + req: MedusaRequest +} -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - describe("GET /custom", () => { - const headers: Record<string, string> = { - } - beforeEach(async () => { - const container = getContainer() - - const authModuleService = container.resolve("auth") - const customerModuleService = container.resolve("customer") - - const customer = await customerModuleService.createCustomers({ - email: "admin@medusa.js", - - }) - const authIdentity = await authModuleService.createAuthIdentities({ - provider_identities: [ - { - provider: "emailpass", - entity_id: "customer@medusa.js", - provider_metadata: { - password: "supersecret", - }, - }, - ], - app_metadata: { - user_id: customer.id, - }, - }) - - const token = jwt.sign( - { - actor_id: customer.id, - actor_type: "customer", - auth_identity_id: authIdentity.id, - }, - "supersecret", - { - expiresIn: "1d", - } - ) - - headers["authorization"] = `Bearer ${token}` +const authenticateUserStep = createStep( + "authenticate-user", + async ({ req }: Input, { container }) => { + const authModuleService = container.resolve(Modules.AUTH) + 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 + ) - const pak = (await createApiKeysWorkflow(getContainer()).run({ - input: { - api_keys: [ - { - type: "publishable", - title: "Test Key", - created_by: "", - }, - ], - }, - })).result[0] + if (!success) { + // incorrect authentication details + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + error || "Incorrect authentication details" + ) + } - headers["x-publishable-api-key"] = pak.token - }) - it("returns correct message", async () => { - const response = await api.get( - `/store/customers/me/custom`, - { headers } - ) - - expect(response.status).toEqual(200) - }) - }) - }) + return new StepResponse({ authIdentity }, authIdentity?.id) }, -}) - -jest.setTimeout(60 * 1000) -``` - -In the test suite, you add a `beforeEach` hook that creates a user or customer, an auth identity, and generates a JWT token for them. The JWT token is then set in the `Authorization` header of the request. - -You also create and pass a publishable API key in the header for the customer as it's required for requests to `/store` routes. Learn more in [this section](#pass-publishable-api-key). - -*** - -## Upload Files in Test Requests - -If your API route requires uploading a file, create a `FormData` object imported from the `form-data` object, then pass the form data headers in the request. - -For example: - -The `form-data` package is available by default. + async (authIdentityId, { container }) => { + if (!authIdentityId) { + return + } + + const authModuleService = container.resolve(Modules.AUTH) -```ts title="integration-tests/http/custom-routes.spec.ts" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import FormData from "form-data" + await authModuleService.deleteAuthIdentities([authIdentityId]) + } +) -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - describe("GET /custom", () => { - it("upload file", async () => { - const form = new FormData() - form.append("files", Buffer.from("content 1"), "image1.jpg") - form.append("files", Buffer.from("content 2"), "image2.jpg") +export const authenticateUserWorkflow = createWorkflow( + "authenticate-user", + (input: Input) => { + const { authIdentity } = authenticateUserStep(input) - const response = await api.post(`/custom`, form, { - headers: form.getHeaders(), - }) - - expect(response.status).toEqual(200) - expect(response.data).toHaveProperty("files") - expect(response.data.files).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - url: expect.any(String), - }), - ]) - ) - }) - }) + return new WorkflowResponse({ + authIdentity, }) - }, -}) - -jest.setTimeout(60 * 1000) + } +) ``` -You don't have to actually upload a file, you use the `form.append` method to append to a `files` field in the form data object, and you pass random content using the `Buffer.from` method. +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -Then, you pass to the `api.post` method the form data object as a second parameter, and an object with the `headers` property set to the form data object's headers as a third parameter. +```ts title="API Route" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { authenticateUserWorkflow } from "../../workflows/authenticate-user" -If you're passing authentication or other headers, you can pass both the form data headers and the authentication headers in the same object: +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await authenticateUserWorkflow(req.scope) + .run({ + req, + }) -```ts title="integration-tests/http/custom-routes.spec.ts" -const response = await api.post(`/custom`, form, { - headers: { - ...form.getHeaders(), - ...authHeaders, - }, -}) + res.send(result) +} ``` +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -# Commerce Modules - -In this section of the documentation, you'll find guides and references related to Medusa's commerce modules. - -A commerce module provides features for a commerce domain within its service. The Medusa application exposes these features in its API routes to clients. +*** -A commerce module also defines data models, representing tables in the database. Medusa's framework and tools allow you to extend these data models to add custom fields. +## Configure Auth Module -## Commerce Modules List +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. *** -## How to Use Modules +## Providers -The Commerce Modules can be used in many use cases, including: +Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types. -- Medusa Application: The Medusa application uses the Commerce Modules to expose commerce features through the REST APIs. -- Serverless Application: Use the Commerce Modules in a serverless application, such as a Next.js application, without having to manage a fully-fledged ecommerce system. You can use it by installing it in your Node.js project as an NPM package. -- Node.js Application: Use the Commerce Modules in any Node.js application by installing it with NPM. +*** -# API Key Module +# Customer Module -In this section of the documentation, you will find resources to learn more about the API Key Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Customer Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/developer/index.html.md) to learn how to manage publishable and secret API keys using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers and groups using the dashboard. -Medusa has API-key related features available out-of-the-box through the API Key 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 API Key Module. +Medusa has customer related features available out-of-the-box through the Customer 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 Customer Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## API Key Features +## Customer Features -- [API Key Types and Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts/index.html.md): Manage API keys in your store. You can create both publishable and secret API keys for different use cases. -- [Token Verification](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#token-verification/index.html.md): Verify tokens of secret API keys to authenticate users or actions. -- [Revoke Keys](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#api-key-expiration/index.html.md): Revoke keys to disable their use permanently. -- Roll API Keys: Roll API keys by [revoking](https://docs.medusajs.com/references/api-key/revoke/index.html.md) a key then [re-creating it](https://docs.medusajs.com/references/api-key/createApiKeys/index.html.md). +- [Customer Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/customer-accounts/index.html.md): Store and manage guest and registered customers in your store. +- [Customer Organization](https://docs.medusajs.com/references/customer/models/index.html.md): Organize customers into groups. This has a lot of benefits and supports many use cases, such as provide discounts for specific customer groups using the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md). *** -## How to Use the API Key Module +## How to Use the Customer 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. @@ -14737,7 +15157,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-api-key.ts" highlights={highlights} +```ts title="src/workflows/create-customer.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -14746,33 +15166,36 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createApiKeyStep = createStep( - "create-api-key", +const createCustomerStep = createStep( + "create-customer", async ({}, { container }) => { - const apiKeyModuleService = container.resolve(Modules.API_KEY) + const customerModuleService = container.resolve(Modules.CUSTOMER) - const apiKey = await apiKeyModuleService.createApiKeys({ - title: "Publishable API key", - type: "publishable", - created_by: "user_123", + const customer = await customerModuleService.createCustomers({ + first_name: "Peter", + last_name: "Hayes", + email: "peter.hayes@example.com", }) - return new StepResponse({ apiKey }, apiKey.id) + return new StepResponse({ customer }, customer.id) }, - async (apiKeyId, { container }) => { - const apiKeyModuleService = container.resolve(Modules.API_KEY) + async (customerId, { container }) => { + if (!customerId) { + return + } + const customerModuleService = container.resolve(Modules.CUSTOMER) - await apiKeyModuleService.deleteApiKeys([apiKeyId]) + await customerModuleService.deleteCustomers([customerId]) } ) -export const createApiKeyWorkflow = createWorkflow( - "create-api-key", +export const createCustomerWorkflow = createWorkflow( + "create-customer", () => { - const { apiKey } = createApiKeyStep() + const { customer } = createCustomerStep() return new WorkflowResponse({ - apiKey, + customer, }) } ) @@ -14787,13 +15210,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createApiKeyWorkflow } from "../../workflows/create-api-key" +import { createCustomerWorkflow } from "../../workflows/create-customer" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createApiKeyWorkflow(req.scope) + const { result } = await createCustomerWorkflow(req.scope) .run() res.send(result) @@ -14807,13 +15230,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createApiKeyWorkflow } from "../workflows/create-api-key" +import { createCustomerWorkflow } from "../workflows/create-customer" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createApiKeyWorkflow(container) + const { result } = await createCustomerWorkflow(container) .run() console.log(result) @@ -14828,12 +15251,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createApiKeyWorkflow } from "../workflows/create-api-key" +import { createCustomerWorkflow } from "../workflows/create-customer" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createApiKeyWorkflow(container) + const { result } = await createCustomerWorkflow(container) .run() console.log(result) @@ -14850,24 +15273,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# 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. @@ -14875,54 +15298,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, }) } ) @@ -14932,19 +15347,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) } @@ -14952,19 +15369,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) } @@ -14976,15 +15395,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) } @@ -15000,24 +15421,27 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Auth Module +# Inventory 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 Inventory 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. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features using the dashboard. + +Medusa has inventory related features available out-of-the-box through the Inventory 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 Inventory 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 +## Inventory 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. +- [Inventory Items Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md): Store and manage inventory of any stock-kept item, such as product variants. +- [Inventory Across Locations](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventorylevel/index.html.md): Manage inventory levels across different locations, such as warehouses. +- [Reservation Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#reservationitem/index.html.md): Reserve quantities of inventory items at specific locations for orders or other purposes. +- [Check Inventory Availability](https://docs.medusajs.com/references/inventory-next/confirmInventory/index.html.md): Check whether an inventory item has the necessary quantity for purchase. +- [Inventory Kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. *** -## How to Use the Auth Module +## How to Use the Inventory 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. @@ -15025,175 +15449,45 @@ 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-inventory-item.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" +import { Modules } from "@medusajs/framework/utils" -type Input = { - req: MedusaRequest -} +const createInventoryItemStep = createStep( + "create-inventory-item", + async ({}, { container }) => { + const inventoryModuleService = container.resolve(Modules.INVENTORY) -const authenticateUserStep = createStep( - "authenticate-user", - async ({ req }: Input, { container }) => { - const authModuleService = container.resolve(Modules.AUTH) + const inventoryItem = await inventoryModuleService.createInventoryItems({ + sku: "SHIRT", + title: "Green Medusa Shirt", + requires_shipping: true, + }) - 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" - ) - } - - 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 { authenticateUserWorkflow } from "../../workflows/authenticate-user" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await authenticateUserWorkflow(req.scope) - .run({ - req, - }) - - res.send(result) -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - -## Configure Auth Module - -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. - -*** - -## Providers - -Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types. - -*** - - -# Customer Module - -In this section of the documentation, you will find resources to learn more about the Customer Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers and groups using the dashboard. - -Medusa has customer related features available out-of-the-box through the Customer 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 Customer Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Customer Features - -- [Customer Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/customer-accounts/index.html.md): Store and manage guest and registered customers in your store. -- [Customer Organization](https://docs.medusajs.com/references/customer/models/index.html.md): Organize customers into groups. This has a lot of benefits and supports many use cases, such as provide discounts for specific customer groups using the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md). - -*** - -## How to Use the Customer 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. - -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-customer.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createCustomerStep = createStep( - "create-customer", - async ({}, { container }) => { - const customerModuleService = container.resolve(Modules.CUSTOMER) - - const customer = await customerModuleService.createCustomers({ - first_name: "Peter", - last_name: "Hayes", - email: "peter.hayes@example.com", - }) - - return new StepResponse({ customer }, customer.id) + return new StepResponse({ inventoryItem }, inventoryItem.id) }, - async (customerId, { container }) => { - if (!customerId) { + async (inventoryItemId, { container }) => { + if (!inventoryItemId) { return } - const customerModuleService = container.resolve(Modules.CUSTOMER) + const inventoryModuleService = container.resolve(Modules.INVENTORY) - await customerModuleService.deleteCustomers([customerId]) + await inventoryModuleService.deleteInventoryItems([inventoryItemId]) } ) -export const createCustomerWorkflow = createWorkflow( - "create-customer", +export const createInventoryItemWorkflow = createWorkflow( + "create-inventory-item-workflow", () => { - const { customer } = createCustomerStep() + const { inventoryItem } = createInventoryItemStep() return new WorkflowResponse({ - customer, + inventoryItem, }) } ) @@ -15208,13 +15502,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createCustomerWorkflow } from "../../workflows/create-customer" +import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createCustomerWorkflow(req.scope) + const { result } = await createInventoryItemWorkflow(req.scope) .run() res.send(result) @@ -15228,13 +15522,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createCustomerWorkflow } from "../workflows/create-customer" +import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createCustomerWorkflow(container) + const { result } = await createInventoryItemWorkflow(container) .run() console.log(result) @@ -15249,12 +15543,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createCustomerWorkflow } from "../workflows/create-customer" +import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createCustomerWorkflow(container) + const { result } = await createInventoryItemWorkflow(container) .run() console.log(result) @@ -15271,154 +15565,6 @@ 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. - -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). - -## Currency 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. - -*** - -## 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. - -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/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 retrieveCurrencyStep = createStep( - "retrieve-currency", - async ({}, { container }) => { - const currencyModuleService = container.resolve(Modules.CURRENCY) - - const currency = await currencyModuleService - .retrieveCurrency("usd") - - return new StepResponse({ currency }) - } -) - -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({ - formattedPrice, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```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 { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await retrievePriceWithCurrency(req.scope) - .run({ - price: 10, - }) - - 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", -} -``` - -### 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" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await retrievePriceWithCurrency(container) - .run({ - price: 10, - }) - - 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). - -*** - - # Fulfillment Module In this section of the documentation, you will find resources to learn more about the Fulfillment Module and how to use it in your application. @@ -15585,150 +15731,6 @@ The Fulfillment Module accepts options for further configurations. Refer to [thi *** -# Inventory Module - -In this section of the documentation, you will find resources to learn more about the Inventory Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features using the dashboard. - -Medusa has inventory related features available out-of-the-box through the Inventory 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 Inventory Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Inventory Features - -- [Inventory Items Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md): Store and manage inventory of any stock-kept item, such as product variants. -- [Inventory Across Locations](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventorylevel/index.html.md): Manage inventory levels across different locations, such as warehouses. -- [Reservation Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#reservationitem/index.html.md): Reserve quantities of inventory items at specific locations for orders or other purposes. -- [Check Inventory Availability](https://docs.medusajs.com/references/inventory-next/confirmInventory/index.html.md): Check whether an inventory item has the necessary quantity for purchase. -- [Inventory Kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. - -*** - -## How to Use the Inventory 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. - -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-inventory-item.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createInventoryItemStep = createStep( - "create-inventory-item", - async ({}, { container }) => { - const inventoryModuleService = container.resolve(Modules.INVENTORY) - - const inventoryItem = await inventoryModuleService.createInventoryItems({ - sku: "SHIRT", - title: "Green Medusa Shirt", - requires_shipping: true, - }) - - return new StepResponse({ inventoryItem }, inventoryItem.id) - }, - async (inventoryItemId, { container }) => { - if (!inventoryItemId) { - return - } - const inventoryModuleService = container.resolve(Modules.INVENTORY) - - await inventoryModuleService.deleteInventoryItems([inventoryItemId]) - } -) - -export const createInventoryItemWorkflow = createWorkflow( - "create-inventory-item-workflow", - () => { - const { inventoryItem } = createInventoryItemStep() - - return new WorkflowResponse({ - inventoryItem, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### 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 { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createInventoryItemWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### 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 { createInventoryItemWorkflow } from "../workflows/create-inventory-item" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createInventoryItemWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createInventoryItemWorkflow(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). - -*** - - # Order Module In this section of the documentation, you will find resources to learn more about the Order Module and how to use it in your application. @@ -17529,6 +17531,124 @@ If the fulfillment provider requires additional custom data to be passed along f The `data` property is an object used to store custom data relevant later for fulfillment. +# Promotions Adjustments in Carts + +In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. + +## What are Adjustment Lines? + +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. + +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. + + + +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. + +*** + +## Discountable Option + +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. + +*** + +## Promotion Actions + +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. + +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). + +For example: + +```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", + ], +}) + +// retrieve line item adjustments +const lineItemAdjustments: ComputeActionItemLine[] = [] +cart.items.forEach((item) => { + const filteredAdjustments = item.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + lineItemAdjustments.push({ + ...item, + adjustments: filteredAdjustments, + }) + } +}) + +// retrieve shipping method adjustments +const shippingMethodAdjustments: ComputeActionShippingLine[] = + [] +cart.shipping_methods.forEach((shippingMethod) => { + const filteredAdjustments = + shippingMethod.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + shippingMethodAdjustments.push({ + ...shippingMethod, + adjustments: filteredAdjustments, + }) + } +}) + +// compute actions +const actions = await promotionModuleService.computeActions( + ["promo_123"], + { + items: lineItemAdjustments, + shipping_methods: shippingMethodAdjustments, + } +) +``` + +The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. + +Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments. + +```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + AddItemAdjustmentAction, + AddShippingMethodAdjustment, + // ... +} from "@medusajs/framework/types" + +// ... + +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[] +) +``` + + # Links between Cart Module and Other Modules This document showcases the module links defined between the Cart Module and other commerce modules. @@ -17966,124 +18086,6 @@ const { data: carts } = useQueryGraphStep({ ``` -# Promotions Adjustments in Carts - -In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. - -## What are Adjustment Lines? - -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. - -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. - - - -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. - -*** - -## Discountable Option - -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. - -*** - -## Promotion Actions - -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. - -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). - -For example: - -```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", - ], -}) - -// retrieve line item adjustments -const lineItemAdjustments: ComputeActionItemLine[] = [] -cart.items.forEach((item) => { - const filteredAdjustments = item.adjustments?.filter( - (adjustment) => adjustment.code !== undefined - ) as unknown as ComputeActionAdjustmentLine[] - if (filteredAdjustments.length) { - lineItemAdjustments.push({ - ...item, - adjustments: filteredAdjustments, - }) - } -}) - -// retrieve shipping method adjustments -const shippingMethodAdjustments: ComputeActionShippingLine[] = - [] -cart.shipping_methods.forEach((shippingMethod) => { - const filteredAdjustments = - shippingMethod.adjustments?.filter( - (adjustment) => adjustment.code !== undefined - ) as unknown as ComputeActionAdjustmentLine[] - if (filteredAdjustments.length) { - shippingMethodAdjustments.push({ - ...shippingMethod, - adjustments: filteredAdjustments, - }) - } -}) - -// compute actions -const actions = await promotionModuleService.computeActions( - ["promo_123"], - { - items: lineItemAdjustments, - shipping_methods: shippingMethodAdjustments, - } -) -``` - -The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. - -Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments. - -```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - AddItemAdjustmentAction, - AddShippingMethodAdjustment, - // ... -} from "@medusajs/framework/types" - -// ... - -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[] -) -``` - - # Tax Lines in Cart Module In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. @@ -18430,544 +18432,151 @@ In the example above, you use the `emailpass` provider, so you have to pass an o If the returned `success` property is `true`, the password has reset successfully. -# How to Create an Actor Type - -In this document, learn how to create an actor type and authenticate its associated data model. +# How to Use Authentication Routes -## 0. Create Module with Data Model +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. -Before creating an actor type, you must have a module with a data model representing the actor type. +These routes are added by Medusa's HTTP layer, not the Auth Module. -Learn how to create a module in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). +## Types of Authentication Flows -The rest of this guide uses this `Manager` data model as an example: +### 1. Basic Authentication Flow -```ts title="src/modules/manager/models/manager.ts" -import { model } from "@medusajs/framework/utils" +This authentication flow doesn't require validation with third-party services. -const Manager = model.define("manager", { - id: model.id().primaryKey(), - firstName: model.text(), - lastName: model.text(), - email: model.text(), -}) +[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). -export default Manager -``` +The steps are: -*** + -## 1. Create Workflow +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). -Start by creating a workflow that does two things: +After registration, you only use the [Auth Route](#login-route) for subsequent authentication. -- 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 handle errors related to existing identities, refer to [this section](#handling-existing-identities). -For example, create the file `src/workflows/create-manager.ts`. with the following content: +### 2. Third-Party Service Authenticate Flow -```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" +This authentication flow authenticates the user with a third-party service, such as Google. -type CreateManagerWorkflowInput = { - manager: { - first_name: string - last_name: string - email: string - } - authIdentityId: string -} +[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). -const createManagerStep = createStep( - "create-manager-step", - async ({ - manager: managerData, - }: Pick<CreateManagerWorkflowInput, "manager">, - { container }) => { - const managerModuleService: ManagerModuleService = - container.resolve("managerModuleService") +It requires the following steps: - const manager = await managerModuleService.createManager( - managerData - ) + - return new StepResponse(manager) - } -) +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. -const createManagerWorkflow = createWorkflow( - "create-manager", - function (input: CreateManagerWorkflowInput) { - const manager = createManagerStep({ - manager: input.manager, - }) +*** - setAuthAppMetadataStep({ - authIdentityId: input.authIdentityId, - actorType: "manager", - value: manager.id, - }) +## Register Route - return new WorkflowResponse(manager) - } -) +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. -export default createManagerWorkflow +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "Whitney_Schultz@gmail.com" + // ... +}' ``` -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. +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. -The workflow has two steps: +For example, if you're registering a customer, you: -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. +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). -*** +### Path Parameters -## 2. Define the Create API Route +Its path parameters are: -Next, you’ll use the workflow defined in the previous section in an API route that creates a manager. +- `{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`. -So, create the file `src/api/manager/route.ts` with the following content: +### Request Body Parameters -```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" +This route accepts in the request body the data that the specified authentication provider requires to handle authentication. -type RequestBody = { - first_name: string - last_name: string - email: string -} +For example, the EmailPass provider requires an `email` and `password` fields in the request body. -export async function POST( - req: AuthenticatedMedusaRequest<RequestBody>, - res: MedusaResponse -) { - // If `actor_id` is present, the request carries - // authentication for an existing manager - if (req.auth_context.actor_id) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Request already authenticated as a manager." - ) - } +### Response Fields - const { result } = await createManagerWorkflow(req.scope) - .run({ - input: { - manager: req.body, - authIdentityId: req.auth_context.auth_identity_id, - }, - }) - - res.status(200).json({ manager: result }) +If the authentication is successful, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." } ``` -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. - -In the API route, you create the manager using the workflow from the previous section and return it in the response. - -*** +Use that token in the header of subsequent requests to send authenticated requests. -## 3. Apply the `authenticate` Middleware +### Handling Existing Identities -The last step is to apply the `authenticate` middleware on the API routes that require a manager’s authentication. +An auth identity with the same email may already exist in Medusa. This can happen if: -To do that, create the file `src/api/middlewares.ts` with the following content: +- 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. -```ts title="src/api/middlewares.ts" highlights={middlewareHighlights} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" +In these scenarios, the Register Route will return an error instead of a token: -export default defineMiddlewares({ - routes: [ - { - matcher: "/manager", - method: "POST", - middlewares: [ - authenticate("manager", ["session", "bearer"], { - allowUnregistered: true, - }), - ], - }, - { - matcher: "/manager/me*", - middlewares: [ - authenticate("manager", ["session", "bearer"]), - ], - }, - ], -}) +```json +{ + "type": "unauthorized", + "message": "Identity with email already exists" +} ``` -This applies middlewares on two route patterns: - -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 Manager API Route - -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" - -export async function GET( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -): Promise<void> { - const managerModuleService: ManagerModuleService = - req.scope.resolve("managerModuleService") +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. - const manager = await managerModuleService.retrieveManager( - req.auth_context.actor_id - ) +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: - res.json({ manager }) +```json +{ + "type": "unauthorized", + "message": "Invalid email or password" } ``` -This route is only accessible by authenticated managers. You access the manager’s ID using `req.auth_context.actor_id`. +You can show that error message to the customer. *** -## Test Custom Actor Type Authentication Flow - -To authenticate managers: +## Login Route -1. Send a `POST` request to `/auth/manager/emailpass/register` to create an auth identity for the manager: +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. ```bash -curl -X POST 'http://localhost:9000/auth/manager/emailpass/register' \ +curl -X POST http://localhost:9000/auth/{actor_type}/{providers} -H 'Content-Type: application/json' \ --data-raw '{ - "email": "manager@gmail.com", - "password": "supersecret" + "email": "Whitney_Schultz@gmail.com" + // ... }' ``` -Copy the returned token to use it in the next request. +For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`. -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" -}' -``` - -Replace `{token}` with the token returned in the previous step. - -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" -}' -``` - -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: - -```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. - -*** - -## Delete User of Actor 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: - -```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" - -export type DeleteManagerWorkflow = { - id: string -} - -const deleteManagerStep = createStep( - "delete-manager-step", - async ( - { id }: DeleteManagerWorkflow, - { container }) => { - const managerModuleService: ManagerModuleService = - container.resolve("managerModuleService") - - const manager = await managerModuleService.retrieve(id) - - await managerModuleService.deleteManagers(id) - - return new StepResponse(undefined, { manager }) - }, - async ({ manager }, { container }) => { - const managerModuleService: ManagerModuleService = - container.resolve("managerModuleService") - - await managerModuleService.createManagers(manager) - } - ) -``` - -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. - -Next, in the same file, add the workflow that deletes a manager: - -```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" - -// ... - -export const deleteManagerWorkflow = createWorkflow( - "delete-manager", - ( - input: WorkflowData<DeleteManagerWorkflow> - ): WorkflowResponse<string> => { - deleteManagerStep(input) - - 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] - - if (!authIdentity) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - "Auth identity not found" - ) - } - - return authIdentity - } - ) - - setAuthAppMetadataStep({ - authIdentityId: authIdentity.id, - actorType: "manager", - value: null, - }) - - return new WorkflowResponse(input.id) - } -) -``` - -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. - -You can use this workflow when deleting a manager, such as in an API route. - - -# 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. - -These routes are added by Medusa's HTTP layer, not the Auth Module. - -## Types of Authentication Flows - -### 1. Basic Authentication Flow - -This authentication flow doesn't require validation with third-party services. - -[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: - - - -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. - -To handle errors related to existing identities, refer to [this section](#handling-existing-identities). - -### 2. Third-Party Service Authenticate Flow - -This authentication flow authenticates the user with a third-party service, such as Google. - -[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). - -It requires the following steps: - - - -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. - -*** - -## Register Route - -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. - -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com" - // ... -}' -``` - -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: - -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). - -### 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, `emailpass`. - -### Request Body Parameters - -This route accepts in the request body the data that the specified authentication provider requires to handle authentication. - -For example, the EmailPass provider requires an `email` and `password` fields in the request body. - -### Response Fields - -If the authentication is successful, you'll receive a `token` field in the response body object: - -```json -{ - "token": "..." -} -``` - -Use that token in the header of subsequent requests to send authenticated requests. - -### Handling Existing Identities - -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. - -In these scenarios, the Register Route will return an error instead of a token: - -```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. - -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: - -```json -{ - "type": "unauthorized", - "message": "Invalid email or password" -} -``` - -You can show that error message to the customer. - -*** - -## Login Route - -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. - -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers} --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com" - // ... -}' -``` - -For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`. - -### Path Parameters +### Path Parameters Its path parameters are: @@ -19215,151 +18824,521 @@ When you specify the `authMethodsPerActor` configuration, it overrides the defau Refer to [this guide](https://docs.medusajs.com/references/auth/provider/index.html.md) to learn how to create an auth module provider. -# Auth Module Options - -In this document, you'll learn about the options of the Auth Module. - -## providers +# How to Create an Actor Type -The `providers` option is an array of auth module providers. +In this document, learn how to create an actor type and authenticate its associated data model. -When the Medusa application starts, these providers are registered and can be used to handle authentication. +## 0. Create Module with Data Model -By default, the `emailpass` provider is registered to authenticate customers and admin users. +Before creating an actor type, you must have a module with a data model representing the actor type. -For example: +Learn how to create a module in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" +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" -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 Manager = model.define("manager", { + id: model.id().primaryKey(), + firstName: model.text(), + lastName: model.text(), + email: model.text(), }) -``` -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. +export default Manager +``` *** -## Auth CORS +## 1. Create Workflow -The Medusa application's authentication API routes are defined under the `/auth` prefix that requires setting the `authCors` property of the `http` configuration. +Start by creating a workflow that does two things: -By default, the Medusa application you created will have an `AUTH_CORS` environment variable, which is used as the value of `authCors`. +- Creates a record of the `Manager` data model. +- Sets the `app_metadata` property of the associated `AuthIdentity` record based on the new actor type. -Refer to [Medusa's configuration guide](https://docs.medusajs.com/references/medusa-config#authCors/index.html.md) to learn more about the `authCors` configuration. +For example, create the file `src/workflows/create-manager.ts`. with the following content: -*** +```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" -## authMethodsPerActor Configuration +type CreateManagerWorkflowInput = { + manager: { + first_name: string + last_name: string + email: string + } + authIdentityId: string +} -The Medusa application's configuration accept an `authMethodsPerActor` configuration which restricts the allowed auth providers used with an actor type. +const createManagerStep = createStep( + "create-manager-step", + async ({ + manager: managerData, + }: Pick<CreateManagerWorkflowInput, "manager">, + { container }) => { + const managerModuleService: ManagerModuleService = + container.resolve("managerModuleService") -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). + const manager = await managerModuleService.createManager( + managerData + ) + return new StepResponse(manager) + } +) -# Customer Accounts +const createManagerWorkflow = createWorkflow( + "create-manager", + function (input: CreateManagerWorkflowInput) { + const manager = createManagerStep({ + manager: input.manager, + }) -In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. + setAuthAppMetadataStep({ + authIdentityId: input.authIdentityId, + actorType: "manager", + value: manager.id, + }) -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. + return new WorkflowResponse(manager) + } +) -## `has_account` Property +export default createManagerWorkflow +``` -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. +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. -When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. +The workflow has two steps: -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`. +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. *** -## Email Uniqueness - -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. - -So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. - +## 2. Define the Create API Route -# How to Handle Password Reset Token Event +Next, you’ll use the workflow defined in the previous section in an API route that creates a manager. -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). +So, create the file `src/api/manager/route.ts` with the following content: -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. +```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" -You'll create a subscriber that listens to the event. When the event is emitted, the subscriber sends an email notification to the user. +type RequestBody = { + first_name: string + last_name: string + email: string +} -### Prerequisites +export async function POST( + req: AuthenticatedMedusaRequest<RequestBody>, + res: MedusaResponse +) { + // If `actor_id` is present, the request carries + // authentication for an existing manager + if (req.auth_context.actor_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Request already authenticated as a manager." + ) + } -- [A notification provider module, such as SendGrid](https://docs.medusajs.com/architectural-modules/notification/sendgrid/index.html.md) + const { result } = await createManagerWorkflow(req.scope) + .run({ + input: { + manager: req.body, + authIdentityId: req.auth_context.auth_identity_id, + }, + }) + + res.status(200).json({ manager: result }) +} +``` -## 1. Create Subscriber +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: -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. +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. -Create the file `src/subscribers/handle-reset.ts` with the following content: +In the API route, you create the manager using the workflow from the previous section and return it in the response. -```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" +*** -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 - ) +## 3. Apply the `authenticate` Middleware - const urlPrefix = actor_type === "customer" ? - "https://storefront.com" : - "https://admin.com/app" +The last step is to apply the `authenticate` middleware on the API routes that require a manager’s authentication. - 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}`, - }, - }) -} +To do that, create the file `src/api/middlewares.ts` with the following content: -export const config: SubscriberConfig = { - event: "auth.password_reset", -} -``` +```ts title="src/api/middlewares.ts" highlights={middlewareHighlights} +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/manager", + method: "POST", + middlewares: [ + authenticate("manager", ["session", "bearer"], { + allowUnregistered: true, + }), + ], + }, + { + matcher: "/manager/me*", + middlewares: [ + authenticate("manager", ["session", "bearer"]), + ], + }, + ], +}) +``` + +This applies middlewares on two route patterns: + +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 Manager API Route + +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" + +export async function GET( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +): Promise<void> { + const managerModuleService: ManagerModuleService = + req.scope.resolve("managerModuleService") + + const manager = await managerModuleService.retrieveManager( + req.auth_context.actor_id + ) + + res.json({ manager }) +} +``` + +This route is only accessible by authenticated managers. You access the manager’s ID using `req.auth_context.actor_id`. + +*** + +## Test Custom Actor Type Authentication Flow + +To authenticate managers: + +1. Send a `POST` request to `/auth/manager/emailpass/register` to create an auth identity for the manager: + +```bash +curl -X POST 'http://localhost:9000/auth/manager/emailpass/register' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "manager@gmail.com", + "password": "supersecret" +}' +``` + +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" +}' +``` + +Replace `{token}` with the token returned in the previous step. + +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" +}' +``` + +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: + +```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. + +*** + +## Delete User of Actor 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: + +```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" + +export type DeleteManagerWorkflow = { + id: string +} + +const deleteManagerStep = createStep( + "delete-manager-step", + async ( + { id }: DeleteManagerWorkflow, + { container }) => { + const managerModuleService: ManagerModuleService = + container.resolve("managerModuleService") + + const manager = await managerModuleService.retrieve(id) + + await managerModuleService.deleteManagers(id) + + return new StepResponse(undefined, { manager }) + }, + async ({ manager }, { container }) => { + const managerModuleService: ManagerModuleService = + container.resolve("managerModuleService") + + await managerModuleService.createManagers(manager) + } + ) +``` + +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. + +Next, in the same file, add the workflow that deletes a manager: + +```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" + +// ... + +export const deleteManagerWorkflow = createWorkflow( + "delete-manager", + ( + input: WorkflowData<DeleteManagerWorkflow> + ): WorkflowResponse<string> => { + deleteManagerStep(input) + + 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] + + if (!authIdentity) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + "Auth identity not found" + ) + } + + return authIdentity + } + ) + + setAuthAppMetadataStep({ + authIdentityId: authIdentity.id, + actorType: "manager", + value: null, + }) + + return new WorkflowResponse(input.id) + } +) +``` + +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. + +You can use this workflow when deleting a manager, such as in an API route. + + +# Auth Module Options + +In this document, you'll learn about the options of the Auth Module. + +## providers + +The `providers` option is an array of auth module providers. + +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. + +For example: + +```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: [ + { + resolve: "@medusajs/medusa/auth-emailpass", + id: "emailpass", + options: { + // provider options... + }, + }, + ], + }, + }, + ], +}) +``` + +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. + +*** + +## Auth CORS + +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`. + +Refer to [Medusa's configuration guide](https://docs.medusajs.com/references/medusa-config#authCors/index.html.md) to learn more about the `authCors` configuration. + +*** + +## authMethodsPerActor Configuration + +The Medusa application's configuration accept an `authMethodsPerActor` configuration which restricts the allowed auth providers used with an actor type. + +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). + + +# How to Handle Password Reset Token Event + +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. + +You'll create a subscriber that listens to the event. When the event is emitted, the subscriber sends an email notification to the user. + +### Prerequisites + +- [A notification provider module, such as SendGrid](https://docs.medusajs.com/architectural-modules/notification/sendgrid/index.html.md) + +## 1. Create Subscriber + +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. + +Create the file `src/subscribers/handle-reset.ts` with the following content: + +```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" + +export default async function resetPasswordTokenHandler({ + event: { data: { + entity_id: email, + token, + actor_type, + } }, + container, +}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) { + const notificationModuleService = container.resolve( + Modules.NOTIFICATION + ) + + const urlPrefix = actor_type === "customer" ? + "https://storefront.com" : + "https://admin.com/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}`, + }, + }) +} + +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: @@ -19418,6 +19397,29 @@ The page shows the user password fields to enter their new password, then submit - [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) +# Customer Accounts + +In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. + +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. + +## `has_account` Property + +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. + +When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. + +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`. + +*** + +## Email Uniqueness + +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. + +So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. + + # Links between Customer Module and Other Modules This document showcases the module links defined between the Customer Module and other commerce modules. @@ -19648,178 +19650,150 @@ const { data: stores } = useQueryGraphStep({ ``` -# Item Fulfillment +# Inventory Concepts -In this document, you’ll learn about the concepts of item fulfillment. +In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. -## Fulfillment Data Model +## InventoryItem -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). +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. -## 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. +### Inventory Shipping Requirement -The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. +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). *** -## data Property +## InventoryLevel -The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. +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. -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. +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. -## Fulfillment Items +### Associated Location -A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model. +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. -The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill. +*** - +## 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. -## Fulfillment Label +The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. -Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the `FulfillmentLabel` data model. -*** +# Inventory Module in Medusa Flows -## Fulfillment Status +This document explains how the Inventory Module is used within the Medusa application's flows. -The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment: +## Product Variant Creation -- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed. -- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped. -- `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered. +When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. +This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) -# 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. +## Add to Cart -## What’s a Fulfillment Module Provider? +When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. -A fulfillment module provider handles fulfilling items, typically using a third-party integration. +This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) -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 +## Order Placed -The Fulfillment Module accepts a `providers` option that allows you to register providers in your application. +When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. -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). +This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) + + *** -## How to Create a Fulfillment Provider? +## Order Fulfillment -Refer to [this guide](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) to learn how to create a fulfillment module provider. +When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: +- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. +- Resets the `reserved_quantity` to `0`. +- Deletes the associated reservation item. -# Fulfillment Concepts +This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) -In this document, you’ll learn about some basic fulfillment concepts. + -## Fulfillment Set +*** -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. +## Order Return -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. +When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. -```ts -const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( - [ - { - name: "Shipping", - type: "shipping", - }, - { - name: "Pick-up", - type: "pick-up", - }, - ] -) -``` +This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) -*** + -## Service Zone +### Dismissed Returned Items -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. +If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. -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. - +# Links between Inventory Module and Other Modules -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. +This document showcases the module links defined between the Inventory Module and other commerce modules. -*** +## Summary -## Shipping Profile +The Inventory Module has the following links to other modules: -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. +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -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. +- [`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). +*** -# Links between Fulfillment Module and Other Modules +## Product Module -This document showcases the module links defined between the Fulfillment Module and other commerce modules. +Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. -## Summary + -The Fulfillment Module has the following links to other modules: +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. -- [`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). +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 -## 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 functionalities. - -Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items. - - - -A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. - - - -### Retrieve with Query - -To retrieve the order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: - -To retrieve the return, pass `return.*` 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 ```ts -const { data: fulfillments } = await query.graph({ - entity: "fulfillment", +const { data: inventoryItems } = await query.graph({ + entity: "inventory_item", fields: [ - "order.*", + "variants.*", ], }) -// fulfillments.order +// inventoryItems.variants ``` ### useQueryGraphStep @@ -19829,19 +19803,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: fulfillments } = useQueryGraphStep({ - entity: "fulfillment", +const { data: inventoryItems } = useQueryGraphStep({ + entity: "inventory_item", fields: [ - "order.*", + "variants.*", ], }) -// fulfillments.order +// inventoryItems.variants ``` ### Manage with Link -To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the variants of an inventory item, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -19851,11 +19825,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", }, }) ``` @@ -19869,40 +19843,36 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", }, }) ``` *** -## Pricing Module - -The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context. - -Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. +## Stock Location Module - +Medusa defines a read-only link between the `InventoryLevel` data model and the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md)'s `StockLocation` data model. This means you can retrieve the details of an inventory level's stock locations, 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 price set of a shipping option with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: +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: shippingOptions } = await query.graph({ - entity: "shipping_option", +const { data: inventoryLevels } = await query.graph({ + entity: "inventory_level", fields: [ - "price_set.*", + "stock_locations.*", ], }) -// shippingOptions.price_set +// inventoryLevels.stock_locations ``` ### useQueryGraphStep @@ -19912,479 +19882,741 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: shippingOptions } = useQueryGraphStep({ - entity: "shipping_option", +const { data: inventoryLevels } = useQueryGraphStep({ + entity: "inventory_level", fields: [ - "price_set.*", + "stock_locations.*", ], }) -// shippingOptions.price_set +// inventoryLevels.stock_locations ``` -### 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): +# Inventory Kits -### link.create +In this guide, you'll learn how inventory kits can be used in the Medusa application to support use cases like multi-part products, bundled products, and shared inventory across products. -```ts -import { Modules } from "@medusajs/framework/utils" +Refer to the following user guides to learn how to use the Medusa Admin dashboard to: -// ... +- [Create Multi-Part Products](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md). +- [Create Bundled Products](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md). -await link.create({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` +## What is an Inventory Kit? -### createRemoteLinkStep +An inventory kit is a collection of inventory items that are linked to a single product variant. These inventory items can be used to represent different parts of a product, or to represent a bundle of products. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +The Medusa application links inventory items from the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to product variants in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). Each variant can have multiple inventory items, and these inventory items can be re-used or shared across variants. -// ... +Using inventory kits, you can implement use cases like: -createRemoteLinkStep({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` +- [Multi-part products](#multi-part-products): A product that consists of multiple parts, each with its own inventory item. +- [Bundled products](#bundled-products): A product that is sold as a bundle, where each variant in the bundle product can re-use the inventory items of another product that should be sold as part of the bundle. *** -## Product Module +## Multi-Part Products -Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile. +Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately. -This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). +To implement this in Medusa, you can: -### Retrieve with Query +- Create inventory items for each of the different parts. +- For each bicycle product, add a variant whose inventory kit consists of the inventory items of each of the parts. -To retrieve the products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: +Then, whenever a customer purchases a bicycle, the inventory of each part is updated accordingly. You can also use the `required_quantity` of the variant's inventory items to set how much quantity is consumed of the part's inventory when a bicycle is sold. For example, the bicycle's wheels require 2 wheels inventory items to be sold when a bicycle is sold. -### query.graph + -```ts -const { data: shippingProfiles } = await query.graph({ - entity: "shipping_profile", - fields: [ - "products.*", - ], -}) +### Create Multi-Part Product -// shippingProfiles.products -``` +Using the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md), you can create a multi-part product by creating its inventory items first, then assigning these inventory items to the product's variant(s). -### useQueryGraphStep +Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the inventory items: -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +```ts highlights={multiPartsHighlights1} +import { + createInventoryItemsWorkflow, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" +import { createWorkflow } from "@medusajs/framework/workflows-sdk" -// ... +export const createMultiPartProductsWorkflow = createWorkflow( + "create-multi-part-products", + () => { + // Alternatively, you can create a stock location + const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: ["*"], + filters: { + name: "European Warehouse", + }, + }) -const { data: shippingProfiles } = useQueryGraphStep({ - entity: "shipping_profile", - fields: [ - "products.*", - ], -}) + const inventoryItems = createInventoryItemsWorkflow.runAsStep({ + input: { + items: [ + { + sku: "FRAME", + title: "Frame", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id, + }, + ], + }, + { + sku: "WHEEL", + title: "Wheel", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id, + }, + ], + }, + { + sku: "SEAT", + title: "Seat", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id, + }, + ], + }, + ], + }, + }) -// shippingProfiles.products + // TODO create the product + } +) ``` -### Manage with Link - -To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +You start by retrieving the stock location to create the inventory items in. Alternatively, you can [create a stock location](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md). -### link.create +Then, you create the inventory items that the product variant consists of. -```ts -import { Modules } from "@medusajs/framework/utils" +Next, create the product and pass the inventory item's IDs to the product's variant: -// ... +```ts highlights={multiPartHighlights2} +import { + // ... + transform, +} from "@medusajs/framework/workflows-sdk" +import { + // ... + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", - }, -}) -``` +export const createMultiPartProductsWorkflow = createWorkflow( + "create-multi-part-products", + () => { + // ... -### createRemoteLinkStep + const inventoryItemIds = transform({ + inventoryItems, + }, (data) => { + return data.inventoryItems.map((inventoryItem) => { + return { + inventory_item_id: inventoryItem.id, + // can also specify required_quantity + } + }) + }) -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Bicycle", + variants: [ + { + title: "Bicycle - Small", + prices: [ + { + amount: 100, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + inventory_items: inventoryItemIds, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + shipping_profile_id: "sp_123", + }, + ], + }, + }) + } +) +``` -// ... +You prepare the inventory item IDs to pass to the variant using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK, then pass these IDs to the created product's variant. -createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", - }, -}) -``` +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). *** -## Stock Location Module +## Bundled Products -The Stock Location Module provides features to manage stock locations in a store. +Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle. -Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location. + - +You can do that by creating a product, where each variant re-uses the inventory items of each of the shirt, pants, and shoes products. -Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. +Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated. - + -### Retrieve with Query +### Create Bundled Product -To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` in `fields`: +You can create a bundled product in the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md) by creating the products part of the bundle first, each having its own inventory items. Then, you create the bundled product whose variant(s) have inventory kits composed of inventory items from each of the products part of the bundle. -To retrieve the stock location of a fulfillment provider, pass `locations.*` in `fields`. +Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the products part of the bundle: -### query.graph +```ts highlights={bundledHighlights1} +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" -```ts -const { data: fulfillmentSets } = await query.graph({ - entity: "fulfillment_set", - fields: [ - "location.*", - ], -}) +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Shirt", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Shirt", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + { + title: "Pants", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Pants", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + { + title: "Shoes", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Shoes", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + ], + }, + }) -// fulfillmentSets.location + // TODO re-retrieve with inventory + } +) ``` -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: fulfillmentSets } = useQueryGraphStep({ - entity: "fulfillment_set", - fields: [ - "location.*", - ], -}) - -// fulfillmentSets.location -``` +You create three products and enable `manage_inventory` for their variants, which will create a default inventory item. You can also create the inventory item first for more control over the quantity as explained in [the previous section](#create-multi-part-product). -### Manage with Link +Next, retrieve the products again but with variant information: -To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +```ts highlights={bundledHighlights2} +import { + // ... + transform, +} from "@medusajs/framework/workflows-sdk" +import { + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" -### link.create +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + // ... + const productIds = transform({ + products, + }, (data) => data.products.map((product) => product.id)) -```ts -import { Modules } from "@medusajs/framework/utils" + // @ts-ignore + const { data: productsWithInventory } = useQueryGraphStep({ + entity: "product", + fields: [ + "variants.*", + "variants.inventory_items.*", + ], + filters: { + id: productIds, + }, + }) -// ... + const inventoryItemIds = transform({ + productsWithInventory, + }, (data) => { + return data.productsWithInventory.map((product) => { + return { + inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id, + } + }) + }) -await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) + // create bundled product + } +) ``` -### 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", - }, -}) -``` +Using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), you retrieve the product again with the inventory items of each variant. Then, you prepare the inventory items to pass to the bundled product's variant. +Finally, create the bundled product: -# Fulfillment Module Options +```ts highlights={bundledProductHighlights3} +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + // ... + const bundledProduct = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Bundled Clothes", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Bundle", + prices: [ + { + amount: 30, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + inventory_items: inventoryItemIds, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + ], + }, + }).config({ name: "create-bundled-product" }) + } +) +``` -In this document, you'll learn about the options of the Fulfillment Module. +The bundled product has the same inventory items as those of the products part of the bundle. -## providers +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). -The `providers` option is an array of fulfillment module providers. -When the Medusa application starts, these providers are registered and can be used to process fulfillments. +# Fulfillment Concepts -For example: +In this document, you’ll learn about some basic fulfillment concepts. -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +## Fulfillment Set -// ... +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. -module.exports = defineConfig({ - // ... - modules: [ +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( + [ { - resolve: "@medusajs/medusa/fulfillment", - options: { - providers: [ - { - resolve: `@medusajs/medusa/fulfillment-manual`, - id: "manual", - options: { - // provider options... - }, - }, - ], - }, + name: "Shipping", + type: "shipping", }, - ], -}) + { + name: "Pick-up", + type: "pick-up", + }, + ] +) ``` -The `providers` option is an array of objects that accept the following properties: +*** -- `resolve`: A string indicating either 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. +## 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. -# Shipping Option +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. -In this document, you’ll learn about shipping options and their rules. + -## What’s a Shipping Option? +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. -A shipping option is a way of shipping an item. Each fulfillment provider provides a set of shipping options. For example, a provider may provide a shipping option for express shipping and another for standard shipping. +*** -When the customer places their order, they choose a shipping option to be used to fulfill their items. +## Shipping Profile -A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). +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. -*** +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. -## Service Zone Restrictions -A shipping option is restricted by a service zone, limiting the locations a shipping option be used in. +# Fulfillment Module Provider -For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada. +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. -Service zones can be more restrictive, such as restricting to certain cities or province codes. +## 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). *** -## Shipping Option Rules +## Configure Fulfillment Providers -You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. +The Fulfillment Module accepts a `providers` option that allows you to register providers in your application. -These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rule: +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). -- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`. -- `operator`: The operator used in the condition. For example: - - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values. - - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values. - - Check out more operators in [this reference](https://docs.medusajs.com/references/fulfillment/types/fulfillment.RuleOperatorType/index.html.md). -- `value`: One or more values. +*** - +## How to Create a Fulfillment Provider? -A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g. +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). *** -## Shipping Profile and Types +## Fulfillment Processing by a Fulfillment Provider -A shipping option belongs to a type. For example, a shipping option’s type may be `express`, while another `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md). +A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items. -A shipping option also belongs to a shipping profile, as each shipping profile defines the type of items to be shipped in a similar manner. +The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. + + *** ## data Property -When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process. +The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. -The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a 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. +*** -# Inventory Concepts +## Fulfillment Items -In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. +A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model. -## InventoryItem +The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill. -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. +*** - +## Fulfillment Label -### Inventory Shipping Requirement +Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the `FulfillmentLabel` data model. -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). +## Fulfillment Status -*** +The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment: -## InventoryLevel +- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed. +- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped. +- `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered. -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: +# Links between Fulfillment Module and Other Modules -- `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. +This document showcases the module links defined between the Fulfillment Module and other commerce modules. -### Associated Location +## Summary -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. +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). *** -## ReservationItem +## Order Module -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 [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management functionalities. -The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. +Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items. + -# Inventory Module in Medusa Flows +A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. -This document explains how the Inventory Module is used within the Medusa application's flows. + -## Product Variant Creation +### Retrieve with Query -When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. +To retrieve the order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: -This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) +To retrieve the return, pass `return.*` in `fields`. - +### query.graph -*** +```ts +const { data: fulfillments } = await query.graph({ + entity: "fulfillment", + fields: [ + "order.*", + ], +}) -## Add to Cart +// fulfillments.order +``` -When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. +### useQueryGraphStep -This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - +// ... -*** +const { data: fulfillments } = useQueryGraphStep({ + entity: "fulfillment", + fields: [ + "order.*", + ], +}) -## Order Placed +// fulfillments.order +``` -When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. +### Manage with Link -This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) +To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - +### link.create -*** +```ts +import { Modules } from "@medusajs/framework/utils" -## Order Fulfillment +// ... -When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", + }, +}) +``` -- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. -- Resets the `reserved_quantity` to `0`. -- Deletes the associated reservation item. +### createRemoteLinkStep -This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - +// ... + +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", + }, +}) +``` *** -## Order Return +## Pricing Module -When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. +The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context. -This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) +Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. - + -### Dismissed Returned Items +### Retrieve with Query -If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. +To retrieve the price set of a shipping option with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: +### query.graph -# Links between Inventory Module and Other Modules +```ts +const { data: shippingOptions } = await query.graph({ + entity: "shipping_option", + fields: [ + "price_set.*", + ], +}) -This document showcases the module links defined between the Inventory Module and other commerce modules. +// shippingOptions.price_set +``` -## Summary +### useQueryGraphStep -The Inventory Module has the following links to other modules: +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -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). +const { data: shippingOptions } = useQueryGraphStep({ + entity: "shipping_option", + fields: [ + "price_set.*", + ], +}) -*** +// shippingOptions.price_set +``` -## Product Module +### Manage with Link -Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. +To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - +### link.create -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. +```ts +import { Modules } from "@medusajs/framework/utils" -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). +// ... + +await link.create({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +*** + +## Product Module + +Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile. + +This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). ### 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 products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: ### query.graph ```ts -const { data: inventoryItems } = await query.graph({ - entity: "inventory_item", +const { data: shippingProfiles } = await query.graph({ + entity: "shipping_profile", fields: [ - "variants.*", + "products.*", ], }) -// inventoryItems.variants +// shippingProfiles.products ``` ### useQueryGraphStep @@ -20394,19 +20626,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: inventoryItems } = useQueryGraphStep({ - entity: "inventory_item", +const { data: shippingProfiles } = useQueryGraphStep({ + entity: "shipping_profile", fields: [ - "variants.*", + "products.*", ], }) -// inventoryItems.variants +// shippingProfiles.products ``` ### Manage with Link -To manage the variants of an inventory item, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -20417,10 +20649,10 @@ import { Modules } from "@medusajs/framework/utils" await link.create({ [Modules.PRODUCT]: { - variant_id: "variant_123", + product_id: "prod_123", }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", }, }) ``` @@ -20435,10 +20667,10 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" createRemoteLinkStep({ [Modules.PRODUCT]: { - variant_id: "variant_123", + product_id: "prod_123", }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", }, }) ``` @@ -20447,23 +20679,33 @@ createRemoteLinkStep({ ## Stock Location Module -Medusa defines a read-only link between the `InventoryLevel` data model and the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md)'s `StockLocation` data model. This means you can retrieve the details of an inventory level's stock locations, 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. +The Stock Location Module provides features to manage stock locations in a store. + +Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location. + + + +Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. + + ### 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`: +To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` in `fields`: + +To retrieve the stock location of a fulfillment provider, pass `locations.*` in `fields`. ### query.graph ```ts -const { data: inventoryLevels } = await query.graph({ - entity: "inventory_level", +const { data: fulfillmentSets } = await query.graph({ + entity: "fulfillment_set", fields: [ - "stock_locations.*", + "location.*", ], }) -// inventoryLevels.stock_locations +// fulfillmentSets.location ``` ### useQueryGraphStep @@ -20473,403 +20715,163 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: inventoryLevels } = useQueryGraphStep({ - entity: "inventory_level", +const { data: fulfillmentSets } = useQueryGraphStep({ + entity: "fulfillment_set", fields: [ - "stock_locations.*", + "location.*", ], }) -// inventoryLevels.stock_locations +// fulfillmentSets.location ``` +### Manage with Link -# Inventory Kits +To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -In this guide, you'll learn how inventory kits can be used in the Medusa application to support use cases like multi-part products, bundled products, and shared inventory across products. +### link.create -Refer to the following user guides to learn how to use the Medusa Admin dashboard to: +```ts +import { Modules } from "@medusajs/framework/utils" -- [Create Multi-Part Products](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md). -- [Create Bundled Products](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md). +// ... -## What is an Inventory Kit? +await link.create({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` -An inventory kit is a collection of inventory items that are linked to a single product variant. These inventory items can be used to represent different parts of a product, or to represent a bundle of products. +### createRemoteLinkStep -The Medusa application links inventory items from the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to product variants in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). Each variant can have multiple inventory items, and these inventory items can be re-used or shared across variants. +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -Using inventory kits, you can implement use cases like: +// ... -- [Multi-part products](#multi-part-products): A product that consists of multiple parts, each with its own inventory item. -- [Bundled products](#bundled-products): A product that is sold as a bundle, where each variant in the bundle product can re-use the inventory items of another product that should be sold as part of the bundle. +createRemoteLinkStep({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` -*** -## Multi-Part Products +# Shipping Option -Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately. +In this document, you’ll learn about shipping options and their rules. -To implement this in Medusa, you can: +## What’s a Shipping Option? -- Create inventory items for each of the different parts. -- For each bicycle product, add a variant whose inventory kit consists of the inventory items of each of the parts. +A shipping option is a way of shipping an item. Each fulfillment provider provides a set of shipping options. For example, a provider may provide a shipping option for express shipping and another for standard shipping. -Then, whenever a customer purchases a bicycle, the inventory of each part is updated accordingly. You can also use the `required_quantity` of the variant's inventory items to set how much quantity is consumed of the part's inventory when a bicycle is sold. For example, the bicycle's wheels require 2 wheels inventory items to be sold when a bicycle is sold. +When the customer places their order, they choose a shipping option to be used to fulfill their items. - +A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). -### Create Multi-Part Product +*** -Using the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md), you can create a multi-part product by creating its inventory items first, then assigning these inventory items to the product's variant(s). +## Service Zone Restrictions -Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the inventory items: +A shipping option is restricted by a service zone, limiting the locations a shipping option be used in. -```ts highlights={multiPartsHighlights1} -import { - createInventoryItemsWorkflow, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" -import { createWorkflow } from "@medusajs/framework/workflows-sdk" +For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada. -export const createMultiPartProductsWorkflow = createWorkflow( - "create-multi-part-products", - () => { - // Alternatively, you can create a stock location - const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: ["*"], - filters: { - name: "European Warehouse", - }, - }) - - const inventoryItems = createInventoryItemsWorkflow.runAsStep({ - input: { - items: [ - { - sku: "FRAME", - title: "Frame", - location_levels: [ - { - stocked_quantity: 100, - location_id: stockLocations[0].id, - }, - ], - }, - { - sku: "WHEEL", - title: "Wheel", - location_levels: [ - { - stocked_quantity: 100, - location_id: stockLocations[0].id, - }, - ], - }, - { - sku: "SEAT", - title: "Seat", - location_levels: [ - { - stocked_quantity: 100, - location_id: stockLocations[0].id, - }, - ], - }, - ], - }, - }) + - // TODO create the product - } -) -``` +Service zones can be more restrictive, such as restricting to certain cities or province codes. -You start by retrieving the stock location to create the inventory items in. Alternatively, you can [create a stock location](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md). + -Then, you create the inventory items that the product variant consists of. +*** -Next, create the product and pass the inventory item's IDs to the product's variant: +## Shipping Option Rules -```ts highlights={multiPartHighlights2} -import { - // ... - transform, -} from "@medusajs/framework/workflows-sdk" -import { - // ... - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" +You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. -export const createMultiPartProductsWorkflow = createWorkflow( - "create-multi-part-products", - () => { - // ... +These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rule: - const inventoryItemIds = transform({ - inventoryItems, - }, (data) => { - return data.inventoryItems.map((inventoryItem) => { - return { - inventory_item_id: inventoryItem.id, - // can also specify required_quantity - } - }) - }) +- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`. +- `operator`: The operator used in the condition. For example: + - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values. + - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values. + - Check out more operators in [this reference](https://docs.medusajs.com/references/fulfillment/types/fulfillment.RuleOperatorType/index.html.md). +- `value`: One or more values. - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Bicycle", - variants: [ - { - title: "Bicycle - Small", - prices: [ - { - amount: 100, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - inventory_items: inventoryItemIds, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - shipping_profile_id: "sp_123", - }, - ], - }, - }) - } -) -``` + -You prepare the inventory item IDs to pass to the variant using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK, then pass these IDs to the created product's variant. +A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g. -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). + *** -## Bundled Products - -Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle. - - - -You can do that by creating a product, where each variant re-uses the inventory items of each of the shirt, pants, and shoes products. - -Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated. - - +## Shipping Profile and Types -### Create Bundled Product +A shipping option belongs to a type. For example, a shipping option’s type may be `express`, while another `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md). -You can create a bundled product in the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md) by creating the products part of the bundle first, each having its own inventory items. Then, you create the bundled product whose variant(s) have inventory kits composed of inventory items from each of the products part of the bundle. +A shipping option also belongs to a shipping profile, as each shipping profile defines the type of items to be shipped in a similar manner. -Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the products part of the bundle: +*** -```ts highlights={bundledHighlights1} -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" +## data Property -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Shirt", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Shirt", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - { - title: "Pants", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Pants", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - { - title: "Shoes", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Shoes", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - ], - }, - }) +When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process. - // TODO re-retrieve with inventory - } -) -``` +The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. -You create three products and enable `manage_inventory` for their variants, which will create a default inventory item. You can also create the inventory item first for more control over the quantity as explained in [the previous section](#create-multi-part-product). -Next, retrieve the products again but with variant information: +# Fulfillment Module Options -```ts highlights={bundledHighlights2} -import { - // ... - transform, -} from "@medusajs/framework/workflows-sdk" -import { - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" +In this document, you'll learn about the options of the Fulfillment Module. -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - // ... - const productIds = transform({ - products, - }, (data) => data.products.map((product) => product.id)) +## providers - // @ts-ignore - const { data: productsWithInventory } = useQueryGraphStep({ - entity: "product", - fields: [ - "variants.*", - "variants.inventory_items.*", - ], - filters: { - id: productIds, - }, - }) +The `providers` option is an array of fulfillment module providers. - const inventoryItemIds = transform({ - productsWithInventory, - }, (data) => { - return data.productsWithInventory.map((product) => { - return { - inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id, - } - }) - }) +When the Medusa application starts, these providers are registered and can be used to process fulfillments. - // create bundled product - } -) -``` +For example: -Using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), you retrieve the product again with the inventory items of each variant. Then, you prepare the inventory items to pass to the bundled product's variant. +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" -Finally, create the bundled product: +// ... -```ts highlights={bundledProductHighlights3} -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - // ... - const bundledProduct = createProductsWorkflow.runAsStep({ - input: { - products: [ +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/fulfillment", + options: { + providers: [ { - title: "Bundled Clothes", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Bundle", - prices: [ - { - amount: 30, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - inventory_items: inventoryItemIds, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], + resolve: `@medusajs/medusa/fulfillment-manual`, + id: "manual", + options: { + // provider options... + }, }, ], }, - }).config({ name: "create-bundled-product" }) - } -) + }, + ], +}) ``` -The bundled product has the same inventory items as those of the products part of the bundle. +The `providers` option is an array of objects that accept the following properties: -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). +- `resolve`: A string indicating either 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. # Order Concepts @@ -20921,60 +20923,6 @@ 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 Claim - -In this document, you’ll learn about order claims. - -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 a Claim? - -When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. - -The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. - -*** - -## Claim Type - -The `Claim` data model has a `type` property whose value indicates the type of the claim: - -- `refund`: the items are returned, and the customer is refunded. -- `replace`: the items are returned, and the customer receives new items. - -*** - -## 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. - -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). - -*** - -## Claim Shipping Methods - -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). - -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). - -*** - -## Claim Refund - -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. - -*** - -## How Claims Impact an Order’s Version - -When a claim is confirmed, the order’s version is incremented. - - # Order Edit In this document, you'll learn about order edits. @@ -21032,98 +20980,6 @@ Once the Order Edit is confirmed, any additional payment or refund required can 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 - -In this document, you’ll learn about order exchanges. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard. - -## What is an Exchange? - -An exchange is the replacement of an item that the customer ordered with another. - -A merchant creates the exchange, specifying the items to be replaced and the new items to be sent. - -The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. - -*** - -## Returned and New Items - -When the exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is created to handle receiving the items back from the customer. - -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). - -The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer. - -*** - -## Exchange Shipping Methods - -An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). - -The shipping methods for the returned items are associated with the exchange'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). - -*** - -## Exchange Payment - -The `Exchange` data model has a `difference_due` property that stores the outstanding amount. - -|Condition|Result| -|---|---|---| -|\`difference\_due \< 0\`|Merchant owes the customer a refund of the | -|\`difference\_due > 0\`|Merchant requires additional payment from the customer of the | -|\`difference\_due = 0\`|No payment processing is required.| - -Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). - -*** - -## How Exchanges Impact an Order’s Version - -When an exchange is confirmed, the order’s version is incremented. - - -# Order Change - -In this document, you'll learn about the Order Change data model and possible actions in it. - -## OrderChange Data Model - -The [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md) represents any kind of change to an order, such as a return, exchange, or edit. - -Its `change_type` property indicates what the order change is created for: - -1. `edit`: The order change is making edits to the order, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md). -2. `exchange`: The order change is associated with an exchange, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md). -3. `claim`: The order change is associated with a claim, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md). -4. `return_request` or `return_receive`: The order change is associated with a return, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). - -Once the order change is confirmed, its changes are applied on the order. - -*** - -## Order Change Actions - -The actions to perform on the original order by a change, such as adding an item, are represented by the [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md). - -The `OrderChangeAction` has an `action` property that indicates the type of action to perform on the order, and a `details` property that holds more details related to the action. - -The following table lists the possible `action` values that Medusa uses and what `details` they carry. - -|Action|Description|Details| -|---|---|---|---|---| -|\`ITEM\_ADD\`|Add an item to the order.|\`details\`| -|\`ITEM\_UPDATE\`|Update an item in the order.|\`details\`| -|\`RETURN\_ITEM\`|Set an item to be returned.|\`details\`| -|\`RECEIVE\_RETURN\_ITEM\`|Mark a return item as received.|\`details\`| -|\`RECEIVE\_DAMAGED\_RETURN\_ITEM\`|Mark a return item that's damaged as received.|\`details\`| -|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | -|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | -|\`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\`| - - # Links between Order Module and Other Modules This document showcases the module links defined between the Order Module and other commerce modules. @@ -21646,25 +21502,94 @@ const { data: orders } = useQueryGraphStep({ ``` -# Promotions Adjustments in Orders - -In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines. +# Order Change -## What are Adjustment Lines? +In this document, you'll learn about the Order Change data model and possible actions in it. -An adjustment line indicates a change to a line item or a shipping method’s amount. It’s used to apply promotions or discounts on an order. +## OrderChange Data Model -The [OrderLineItemAdjustment data model](https://docs.medusajs.com/references/order/models/OrderLineItemAdjustment/index.html.md) represents changes on a line item, and the [OrderShippingMethodAdjustment data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodAdjustment/index.html.md) represents changes on a shipping method. +The [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md) represents any kind of change to an order, such as a return, exchange, or edit. - +Its `change_type` property indicates what the order change is created for: -The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. +1. `edit`: The order change is making edits to the order, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md). +2. `exchange`: The order change is associated with an exchange, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md). +3. `claim`: The order change is associated with a claim, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md). +4. `return_request` or `return_receive`: The order change is associated with a return, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). -The ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. +Once the order change is confirmed, its changes are applied on the order. *** -## Discountable Option +## Order Change Actions + +The actions to perform on the original order by a change, such as adding an item, are represented by the [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md). + +The `OrderChangeAction` has an `action` property that indicates the type of action to perform on the order, and a `details` property that holds more details related to the action. + +The following table lists the possible `action` values that Medusa uses and what `details` they carry. + +|Action|Description|Details| +|---|---|---|---|---| +|\`ITEM\_ADD\`|Add an item to the order.|\`details\`| +|\`ITEM\_UPDATE\`|Update an item in the order.|\`details\`| +|\`RETURN\_ITEM\`|Set an item to be returned.|\`details\`| +|\`RECEIVE\_RETURN\_ITEM\`|Mark a return item as received.|\`details\`| +|\`RECEIVE\_DAMAGED\_RETURN\_ITEM\`|Mark a return item that's damaged as received.|\`details\`| +|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | +|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | +|\`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\`| + + +# Order Versioning + +In this document, you’ll learn how an order and its details are versioned. + +## What's Versioning? + +Versioning means assigning a version number to a record, such as an order and its items. This is useful to view the different versions of the order following changes in its lifetime. + +When changes are made on an order, such as an item is added or returned, the order's version changes. + +*** + +## version Property + +The `Order` and `OrderSummary` data models have a `version` property that indicates the current version. By default, its value is `1`. + +Other order-related data models, such as `OrderItem`, also has a `version` property, but it indicates the version it belongs to. + +*** + +## How the Version Changes + +When the order is changed, such as an item is exchanged, this changes the version of the order and its related data: + +1. The version of the order and its summary is incremented. +2. Related order data that have a `version` property, such as the `OrderItem`, are duplicated. The duplicated item has the new version, whereas the original item has the previous version. + +When the order is retrieved, only the related data having the same version is retrieved. + + +# Promotions Adjustments in Orders + +In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines. + +## What are Adjustment Lines? + +An adjustment line indicates a change to a line item or a shipping method’s amount. It’s used to apply promotions or discounts on an order. + +The [OrderLineItemAdjustment data model](https://docs.medusajs.com/references/order/models/OrderLineItemAdjustment/index.html.md) represents changes on a line item, and the [OrderShippingMethodAdjustment data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodAdjustment/index.html.md) represents changes on a shipping method. + + + +The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. + +The ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. + +*** + +## Discountable Option The `OrderLineItem` data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. @@ -21768,97 +21693,6 @@ await orderModuleService.setOrderShippingMethodAdjustments( ``` -# Order Versioning - -In this document, you’ll learn how an order and its details are versioned. - -## What's Versioning? - -Versioning means assigning a version number to a record, such as an order and its items. This is useful to view the different versions of the order following changes in its lifetime. - -When changes are made on an order, such as an item is added or returned, the order's version changes. - -*** - -## version Property - -The `Order` and `OrderSummary` data models have a `version` property that indicates the current version. By default, its value is `1`. - -Other order-related data models, such as `OrderItem`, also has a `version` property, but it indicates the version it belongs to. - -*** - -## How the Version Changes - -When the order is changed, such as an item is exchanged, this changes the version of the order and its related data: - -1. The version of the order and its summary is incremented. -2. Related order data that have a `version` property, such as the `OrderItem`, are duplicated. The duplicated item has the new version, whereas the original item has the previous version. - -When the order is retrieved, only the related data having the same version is retrieved. - - -# Order Return - -In this document, you’ll learn about order returns. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard. - -## What is a Return? - -A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md). - -A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow. - - - -Once the merchant receives the returned items, they mark the return as received. - -*** - -## Returned Items - -The items to be returned are represented by the [ReturnItem data model](references/order/models/ReturnItem). - -The `ReturnItem` model has two properties storing the item's quantity: - -1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity. -2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity. - -*** - -## Return Shipping Methods - -A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](references/order/models/OrderShippingMethod). - -In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled. - -*** - -## Refund Payment - -The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer. - -The [OrderTransaction data model](references/order/models/OrderTransaction) represents the refunds made for the return. - -*** - -## Returns in Exchanges and Claims - -When a merchant creates an exchange or a claim, it includes returning items from the customer. - -The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for. - -*** - -## How Returns Impact an Order’s Version - -The order’s version is incremented when: - -1. A return is requested. -2. A return is marked as received. - - # Tax Lines in Order Module In this document, you’ll learn about tax lines in an order. @@ -21936,6 +21770,47 @@ 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`. +# Account Holders and Saved Payment Methods + +In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. + +Account holders are available starting from Medusa `v2.5.0`. + +## What's an Account Holder? + +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. + +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. + +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. + +*** + +## Save Payment Methods + +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. + +Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). + +*** + +## Account Holder in Medusa Payment Flows + +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. + +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. + +This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). + + # Links between Payment Module and Other Modules This document showcases the module links defined between the Payment Module and other commerce modules. @@ -22280,47 +22155,6 @@ createRemoteLinkStep({ ``` -# Account Holders and Saved Payment Methods - -In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. - -Account holders are available starting from Medusa `v2.5.0`. - -## What's an Account Holder? - -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. - -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. - -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. - -*** - -## Save Payment Methods - -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. - -Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). - -*** - -## Account Holder in Medusa Payment Flows - -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. - -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. - -This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). - - # Payment Module Options In this document, you'll learn about the options of the Payment Module. @@ -22411,6 +22245,43 @@ A payment can be refunded multiple times, and each time a refund record is creat  +# Payment Collection + +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? + +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). + +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. + +*** + +## Multiple Payments + +The payment collection supports multiple payment sessions and payments. + +You can use this to accept payments in increments or split payments across payment providers. + + + +*** + +## Usage with the Cart Module + +The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment. + +During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing. + +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). + + + + # Accept Payment Flow In this document, you’ll learn how to implement an accept-payment flow using workflows or the Payment Module's main service. @@ -22629,41 +22500,37 @@ When the Medusa application starts and registers the payment providers, it also This data model is used to reference a payment provider and determine whether it’s installed in the application. -# Payment Collection +# Payment Session -In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module. +In this document, you’ll learn what a payment session is. -## What's a Payment Collection? +## What's a Payment Session? -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). +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. -Every purchase or request for payment starts with a payment collection. The collection holds details necessary to complete the payment, including: +A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. -- 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. + *** -## Multiple Payments - -The payment collection supports multiple payment sessions and payments. +## data Property -You can use this to accept payments in increments or split payments across payment providers. +Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data. - +For example, the customer's ID in Stripe is stored in the `data` property. *** -## Usage with the Cart Module - -The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment. - -During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing. +## Payment Session Status -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). +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. # Webhook Events @@ -22702,60 +22569,172 @@ If the event's details indicate that the payment should be captured, then the [c 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. -# Payment Session +# Order Exchange -In this document, you’ll learn what a payment session is. +In this document, you’ll learn about order exchanges. -## What's a Payment Session? +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard. -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. +## What is an Exchange? -A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. +An exchange is the replacement of an item that the customer ordered with another. - +A merchant creates the exchange, specifying the items to be replaced and the new items to be sent. + +The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. *** -## data Property +## Returned and New Items -Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data. +When the exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is created to handle receiving the items back from the customer. -For example, the customer's ID in Stripe is stored in the `data` property. +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). + +The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer. *** -## Payment Session Status +## Exchange Shipping Methods -The `status` property of a payment session indicates its current status. Its value can be: +An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). -- `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. +The shipping methods for the returned items are associated with the exchange'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). +*** -# Pricing Concepts +## Exchange Payment -In this document, you’ll learn about the main concepts in the Pricing Module. +The `Exchange` data model has a `difference_due` property that stores the outstanding amount. -## Price Set +|Condition|Result| +|---|---|---| +|\`difference\_due \< 0\`|Merchant owes the customer a refund of the | +|\`difference\_due > 0\`|Merchant requires additional payment from the customer of the | +|\`difference\_due = 0\`|No payment processing is required.| -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). +Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). -Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). +*** - +## How Exchanges Impact an Order’s Version + +When an exchange is confirmed, the order’s version is incremented. + + +# Order Claim + +In this document, you’ll learn about order claims. + +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 a Claim? + +When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. + +The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. *** -## Price List +## Claim Type -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. +The `Claim` data model has a `type` property whose value indicates the type of the claim: -A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied. +- `refund`: the items are returned, and the customer is refunded. +- `replace`: the items are returned, and the customer receives new items. -Its associated prices are represented by the `Price` data model. +*** + +## 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. + +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). + +*** + +## Claim Shipping Methods + +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). + +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). + +*** + +## Claim Refund + +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. + +*** + +## How Claims Impact an Order’s Version + +When a claim is confirmed, the order’s version is incremented. + + +# Order Return + +In this document, you’ll learn about order returns. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard. + +## What is a Return? + +A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md). + +A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow. + + + +Once the merchant receives the returned items, they mark the return as received. + +*** + +## Returned Items + +The items to be returned are represented by the [ReturnItem data model](references/order/models/ReturnItem). + +The `ReturnItem` model has two properties storing the item's quantity: + +1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity. +2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity. + +*** + +## Return Shipping Methods + +A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](references/order/models/OrderShippingMethod). + +In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled. + +*** + +## Refund Payment + +The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer. + +The [OrderTransaction data model](references/order/models/OrderTransaction) represents the refunds made for the return. + +*** + +## Returns in Exchanges and Claims + +When a merchant creates an exchange or a claim, it includes returning items from the customer. + +The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for. + +*** + +## How Returns Impact an Order’s Version + +The order’s version is incremented when: + +1. A return is requested. +2. A return is marked as received. # Links between Pricing Module and Other Modules @@ -22940,35 +22919,27 @@ createRemoteLinkStep({ ``` -# Price Rules - -In this document, you'll learn about price rules for price sets and price lists. - -## Price Rule - -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. +# Pricing Concepts -For exmaple, you create a price restricted to `10557` zip codes. +In this document, you’ll learn about the main concepts in the Pricing Module. - +## Price Set -A price can have multiple price rules. +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, a price can be restricted by a region and a zip code. +Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). - + *** -## Price List Rules +## Price List -Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md). +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. -The `rules_count` property of a `PriceList` indicates how many rules are applied to it. +A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied. - +Its associated prices are represented by the `Price` data model. # Prices Calculation @@ -23164,6 +23135,37 @@ const price = await pricingModuleService.calculatePrices( ### Result +# Price Rules + +In this document, you'll learn about price rules for price sets and price lists. + +## Price Rule + +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. + +For exmaple, you create a price restricted to `10557` zip codes. + + + +A price can have multiple price rules. + +For example, a price can be restricted by a region and a zip code. + + + +*** + +## Price List Rules + +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. + + + + # Tax-Inclusive Pricing In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. @@ -23676,57 +23678,6 @@ createRemoteLinkStep({ ``` -# Configure Selling Products - -In this guide, you'll learn how to set up and configure your products based on their shipping and inventory requirements, the product type, how you want to sell them, or your commerce ecosystem. - -The concepts in this guide are applicable starting from Medusa v2.5.1. - -## Scenario - -Businesses can have different selling requirements: - -1. They may sell physical or digital items. -2. They may sell items that don't require shipping or inventory management, such as selling digital products, services, or booking appointments. -3. They may sell items whose inventory is managed by an external system, such as an ERP. - -Medusa supports these different selling requirements by allowing you to configure shipping and inventory requirements for products and their variants. This guide explains how these configurations work, then provides examples of setting up different use cases. - -*** - -## Configuring Shipping Requirements - -The Medusa application defines a link between the `Product` data model and a [ShippingProfile](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts#shipping-profile/index.html.md) in the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md), allowing you to associate a product with a shipping profile. - -When a product is associated with a shipping profile, its variants require shipping and fulfillment when purchased. This is useful for physical products or digital products that require custom fulfillment. - -If a product doesn't have an associated shipping profile, its variants don't require shipping and fulfillment when purchased. This is useful for digital products, for example, that don't require shipping. - -### Overriding Shipping Requirements for Variants - -A product variant whose inventory is managed by Medusa (its `manage_inventory` property is enabled) has an [inventory item](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventoryitem/index.html.md). The inventory item has a `requires_shipping` property that can be used to override its shipping requirement. This is useful if the product has an associated shipping profile but you want to disable shipping for a specific variant, or vice versa. - -Learn more about product variant's inventory in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). - -When a product variant is purchased, the Medusa application decides whether the purchased item requires shipping in the following order: - -1. The product variant has an inventory item. In this case, the Medusa application uses the inventory item's `requires_shipping` property to determine if the item requires shipping. -2. If the product variant doesn't have an inventory item, the Medusa application checks whether the product has an associated shipping profile to determine if the item requires shipping. - -*** - -## Use Case Examples - -By combining configurations of shipment requirements and inventory management, you can set up your products to support your use case: - -|Use Case|Configurations|Example| -|---|---|---|---|---| -|Item that's shipped on purchase, and its variant inventory is managed by the Medusa application.||Any stock-kept item (clothing, for example), whose inventory is managed in the Medusa application.| -|Item that's shipped on purchase, but its variant inventory is managed externally (not by Medusa) or it has infinite stock.||Any stock-kept item (clothing, for example), whose inventory is managed in an ERP or has infinite stock.| -|Item that's not shipped on purchase, but its variant inventory is managed by Medusa.||Digital products, such as licenses, that don't require shipping but have a limited quantity.| -|Item that doesn't require shipping and its variant inventory isn't managed by Medusa.||| - - # Product Variant Inventory # Product Variant Inventory @@ -23793,6 +23744,57 @@ The following guides provide more details on inventory management in the Medusa - [Storefront guide: how to retrieve a product variant's inventory details](https://docs.medusajs.com/resources/storefront-development/products/inventory/index.html.md). +# Configure Selling Products + +In this guide, you'll learn how to set up and configure your products based on their shipping and inventory requirements, the product type, how you want to sell them, or your commerce ecosystem. + +The concepts in this guide are applicable starting from Medusa v2.5.1. + +## Scenario + +Businesses can have different selling requirements: + +1. They may sell physical or digital items. +2. They may sell items that don't require shipping or inventory management, such as selling digital products, services, or booking appointments. +3. They may sell items whose inventory is managed by an external system, such as an ERP. + +Medusa supports these different selling requirements by allowing you to configure shipping and inventory requirements for products and their variants. This guide explains how these configurations work, then provides examples of setting up different use cases. + +*** + +## Configuring Shipping Requirements + +The Medusa application defines a link between the `Product` data model and a [ShippingProfile](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts#shipping-profile/index.html.md) in the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md), allowing you to associate a product with a shipping profile. + +When a product is associated with a shipping profile, its variants require shipping and fulfillment when purchased. This is useful for physical products or digital products that require custom fulfillment. + +If a product doesn't have an associated shipping profile, its variants don't require shipping and fulfillment when purchased. This is useful for digital products, for example, that don't require shipping. + +### Overriding Shipping Requirements for Variants + +A product variant whose inventory is managed by Medusa (its `manage_inventory` property is enabled) has an [inventory item](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventoryitem/index.html.md). The inventory item has a `requires_shipping` property that can be used to override its shipping requirement. This is useful if the product has an associated shipping profile but you want to disable shipping for a specific variant, or vice versa. + +Learn more about product variant's inventory in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). + +When a product variant is purchased, the Medusa application decides whether the purchased item requires shipping in the following order: + +1. The product variant has an inventory item. In this case, the Medusa application uses the inventory item's `requires_shipping` property to determine if the item requires shipping. +2. If the product variant doesn't have an inventory item, the Medusa application checks whether the product has an associated shipping profile to determine if the item requires shipping. + +*** + +## Use Case Examples + +By combining configurations of shipment requirements and inventory management, you can set up your products to support your use case: + +|Use Case|Configurations|Example| +|---|---|---|---|---| +|Item that's shipped on purchase, and its variant inventory is managed by the Medusa application.||Any stock-kept item (clothing, for example), whose inventory is managed in the Medusa application.| +|Item that's shipped on purchase, but its variant inventory is managed externally (not by Medusa) or it has infinite stock.||Any stock-kept item (clothing, for example), whose inventory is managed in an ERP or has infinite stock.| +|Item that's not shipped on purchase, but its variant inventory is managed by Medusa.||Digital products, such as licenses, that don't require shipping but have a limited quantity.| +|Item that doesn't require shipping and its variant inventory isn't managed by Medusa.||| + + # Links between Region Module and Other Modules This document showcases the module links defined between the Region Module and other commerce modules. @@ -23971,6 +23973,30 @@ createRemoteLinkStep({ ``` +# 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). + + # Links between Sales Channel Module and Other Modules This document showcases the module links defined between the Sales Channel Module and other commerce modules. @@ -24317,47 +24343,6 @@ createRemoteLinkStep({ ``` -# 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. - -## Stock Location - -A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. - -Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. - -*** - -## StockLocationAddress - -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 This document showcases the module links defined between the Stock Location Module and other commerce modules. @@ -24586,6 +24571,23 @@ createRemoteLinkStep({ ``` +# Stock Location Concepts + +In this document, you’ll learn about the main concepts in the Stock Location Module. + +## Stock Location + +A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. + +Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. + +*** + +## StockLocationAddress + +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. + + # Application Method In this document, you'll learn what an application method is. @@ -24623,32 +24625,6 @@ The application method has a collection of `PromotionRule` items to define the 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? - -A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines promotions under the same conditions, such as start and end dates. - - - -*** - -## Campaign Limits - -Each campaign has a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times the promotion can be used. - -There are two types of budgets: - -- `spend`: An amount that, when crossed, the promotion becomes unusable. For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied. -- `usage`: The number of times that a promotion can be used. For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied. - - - - # Promotion Actions In this document, you’ll learn about promotion actions and how they’re computed using the [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). @@ -24760,6 +24736,32 @@ 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. +# 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? + +A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines promotions under the same conditions, such as start and end dates. + + + +*** + +## Campaign Limits + +Each campaign has a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times the promotion can be used. + +There are two types of budgets: + +- `spend`: An amount that, when crossed, the promotion becomes unusable. For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied. +- `usage`: The number of times that a promotion can be used. For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied. + + + + # Promotion Concepts In this document, you’ll learn about the main promotion and rule concepts in the Promotion Module. @@ -25170,49 +25172,6 @@ if (!count) { ``` -# Tax Module Options - -In this document, you'll learn about the options of the Tax Module. - -## providers - -The `providers` option is an array of either tax module providers or path to a file that defines a tax provider. - -When the Medusa application starts, these providers are registered and can be used to retrieve tax lines. - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/tax", - options: { - providers: [ - { - resolve: "./path/to/my-provider", - id: "my-provider", - options: { - // ... - }, - }, - ], - }, - }, - ], -}) -``` - -The objects in the array accept the following properties: - -- `resolve`: A string indicating the package name of the module provider or the path to it. -- `id`: A string indicating the provider's unique name or ID. -- `options`: An optional object of the module provider's options. - - # Tax Calculation with the Tax Provider In this document, you’ll learn how tax lines are calculated and what a tax provider is. @@ -25290,6 +25249,49 @@ TODO add once tax provider guide is updated + add module providers match other m Refer to [this guide](/modules/tax/provider) to learn more about creating a tax provider. */} +# Tax Module Options + +In this document, you'll learn about the options of the Tax Module. + +## providers + +The `providers` option is an array of either tax module providers or path to a file that defines a tax provider. + +When the Medusa application starts, these providers are registered and can be used to retrieve tax lines. + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/tax", + options: { + providers: [ + { + resolve: "./path/to/my-provider", + id: "my-provider", + options: { + // ... + }, + }, + ], + }, + }, + ], +}) +``` + +The objects in the array accept the following properties: + +- `resolve`: A string indicating the package name of the module provider or the path to it. +- `id`: A string indicating the provider's unique name or ID. +- `options`: An optional object of the module provider's options. + + # Tax Rates and Rules In this document, you’ll learn about tax rates and rules. @@ -25405,6 +25407,88 @@ const hashConfig = \{ - [How to register a customer using email and password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/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=<YOUR_GITHUB_CLIENT_ID> +GITHUB_CLIENT_SECRET=<YOUR_GITHUB_CLIENT_SECRET> +GITHUB_CALLBACK_URL=<YOUR_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). + + # Google Auth Module Provider In this document, you’ll learn about the Google Auth Module Provider and how to install and use it in the Auth Module. @@ -25492,86 +25576,84 @@ 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). - -*** +# Get Product Variant Prices using Query -## Register the Github Auth Module Provider +In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). -### Prerequisites +The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model. -- [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) +So, to retrieve data across the linked records of the two modules, you use Query. -Add the module to the array of providers passed to the Auth Module: +## Retrieve All Product Variant Prices -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" +To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`. -// ... +For example: -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, - }, - }, - ], - }, - }, +```ts highlights={[["6"]]} +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "*", + "variants.*", + "variants.prices.*", ], + filters: { + id: [ + "prod_123", + ], + }, }) ``` -### Environment Variables +Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). -Make sure to add the necessary environment variables for the above options in `.env`: +*** -```plain -GITHUB_CLIENT_ID=<YOUR_GITHUB_CLIENT_ID> -GITHUB_CLIENT_SECRET=<YOUR_GITHUB_CLIENT_SECRET> -GITHUB_CALLBACK_URL=<YOUR_GITHUB_CALLBACK_URL> -``` +## Retrieve Calculated Price for a Context -### Module Options +The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code. -|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| +Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md). -*** +To retrieve calculated prices of variants based on a context, retrieve the products using Query and: -## Override Callback URL During Authentication +- Pass `variants.calculated_price.*` in the `fields` property. +- Pass a `context` property in the object parameter. Its value is an object of objects that sets the context for the retrieved fields. -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. +For example: -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). +```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]} +import { QueryContext } from "@medusajs/framework/utils" -*** +// ... -## Examples +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "*", + "variants.*", + "variants.calculated_price.*", + ], + filters: { + id: "prod_123", + }, + context: { + variants: { + calculated_price: QueryContext({ + region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS", + currency_code: "eur", + }), + }, + }, +}) +``` -- [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). +For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`. + +`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md). + +Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). # Stripe Module Provider @@ -25684,86 +25766,6 @@ When you set up the webhook in Stripe, choose the following events to listen to: - [Customize Stripe Integration in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/guides/customize-stripe/index.html.md). -# Get Product Variant Prices using Query - -In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). - -The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model. - -So, to retrieve data across the linked records of the two modules, you use Query. - -## Retrieve All Product Variant Prices - -To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`. - -For example: - -```ts highlights={[["6"]]} -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "*", - "variants.*", - "variants.prices.*", - ], - filters: { - id: [ - "prod_123", - ], - }, -}) -``` - -Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). - -*** - -## Retrieve Calculated Price for a Context - -The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code. - -Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md). - -To retrieve calculated prices of variants based on a context, retrieve the products using Query and: - -- Pass `variants.calculated_price.*` in the `fields` property. -- Pass a `context` property in the object parameter. Its value is an object of objects that sets the context for the retrieved fields. - -For example: - -```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]} -import { QueryContext } from "@medusajs/framework/utils" - -// ... - -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "*", - "variants.*", - "variants.calculated_price.*", - ], - filters: { - id: "prod_123", - }, - context: { - variants: { - calculated_price: QueryContext({ - region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS", - currency_code: "eur", - }), - }, - }, -}) -``` - -For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`. - -`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md). - -Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). - - # Calculate Product Variant Price with Taxes In this document, you'll learn how to calculate a product variant's price with taxes. @@ -25951,353 +25953,388 @@ For each product variant, you: ## Workflows -- [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/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) +- [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/index.html.md) - [updateApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateApiKeysWorkflow/index.html.md) +- [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/index.html.md) +- [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/index.html.md) - [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/index.html.md) +- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/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) -- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/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) +- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/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) +- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/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) +- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) +- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) +- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/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) - [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/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) - [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) +- [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/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) -- [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md) - [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) -- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) -- [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md) - [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/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) - [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md) +- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/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) -- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) -- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md) -- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/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) -- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) -- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/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) - [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md) -- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) +- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md) +- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/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) -- [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md) - [cancelFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelFulfillmentWorkflow/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) +- [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md) - [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/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) +- [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/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) -- [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) +- [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/index.html.md) +- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md) - [updateFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateFulfillmentWorkflow/index.html.md) - [updateShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionsWorkflow/index.html.md) -- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md) -- [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/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) -- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/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) +- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) +- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md) +- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/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) -- [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md) +- [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md) - [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md) -- [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md) +- [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md) - [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/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) -- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) -- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md) -- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md) +- [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md) +- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) +- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) +- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/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) +- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) +- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/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) +- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md) +- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md) +- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) +- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) +- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md) +- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md) +- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/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) +- [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/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) -- [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md) -- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/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) +- [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md) - [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/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) +- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/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) - [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md) -- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/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) +- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/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) - [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) -- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md) - [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md) -- [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/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) -- [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md) -- [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/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) - [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) -- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md) +- [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/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) - [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) -- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/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) - [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md) +- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md) - [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md) - [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/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) -- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) - [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md) +- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) +- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) - [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md) -- [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md) - [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md) -- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/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) +- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md) - [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md) +- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/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) - [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/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) - [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) -- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md) +- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/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) -- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md) - [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md) -- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md) -- [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md) +- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/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) +- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md) - [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/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) -- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) -- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md) - [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) +- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/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) +- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) - [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md) -- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md) - [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) - [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md) -- [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/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) - [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md) -- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md) -- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/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) +- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/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) -- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md) - [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md) -- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md) +- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md) - [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md) -- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md) +- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md) - [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md) - [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md) - [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md) -- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) -- [removeClaimAddItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimAddItemActionValidationStep/index.html.md) - [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) +- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/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) -- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/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) +- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md) - [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md) - [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md) -- [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md) +- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md) - [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md) -- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md) +- [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md) - [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md) - [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md) +- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md) - [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md) - [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md) -- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/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) +- [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/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) -- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/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) +- [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/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) +- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md) - [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/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) -- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md) - [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md) - [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) - [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md) -- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/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) - [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md) - [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md) - [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md) -- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) -- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) - [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md) +- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) +- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) - [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md) - [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md) -- [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md) - [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/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) - [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/index.html.md) +- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) - [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md) - [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) - [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) - [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md) -- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md) +- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md) - [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/index.html.md) - [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md) -- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) -- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) -- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/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) -- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/index.html.md) -- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/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) -- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) -- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md) +- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/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) -- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/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) -- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md) -- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/index.html.md) -- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md) +- [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/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) - [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md) - [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md) -- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/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) - [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md) - [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/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) +- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md) - [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) -- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) -- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/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) +- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) - [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) - [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md) -- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/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) -- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/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) +- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) - [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/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) -- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) -- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/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) - [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md) -- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md) +- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/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) -- [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) +- [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) - [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/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) -- [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/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) - [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md) - [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/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) -- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md) -- [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/index.html.md) - [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md) +- [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/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) -- [updateReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnReasonsWorkflow/index.html.md) -- [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/index.html.md) +- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/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) - [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) - [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/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) -- [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md) -- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/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) +- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/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) -- [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/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) +- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md) - [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/index.html.md) -- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) -- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/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) - [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md) +- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) - [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md) +- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/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) -- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/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) -- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md) +- [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md) - [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) -- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md) - [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) +- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/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) ## Steps - [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) +- [linkSalesChannelsToApiKeyStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkSalesChannelsToApiKeyStep/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) -- [dismissRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/dismissRemoteLinkStep/index.html.md) +- [updateApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateApiKeysStep/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) +- [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) +- [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) +- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/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) +- [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) +- [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) +- [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md) +- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) +- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) +- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/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) +- [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) +- [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/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) +- [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) +- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/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) +- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/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) +- [dismissRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/dismissRemoteLinkStep/index.html.md) - [emitEventStep](https://docs.medusajs.com/references/medusa-workflows/steps/emitEventStep/index.html.md) +- [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/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) - [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md) @@ -26309,23 +26346,20 @@ For each product variant, you: - [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md) - [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/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) - [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md) -- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) +- [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md) +- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/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) -- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/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) - [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) @@ -26339,86 +26373,54 @@ For each product variant, you: - [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) - [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md) -- [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/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) - [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/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) +- [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) - [adjustInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/adjustInventoryLevelsStep/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) - [deleteInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryLevelsStep/index.html.md) -- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/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) -- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md) -- [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/index.html.md) - [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md) -- [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/index.html.md) -- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md) -- [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/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) -- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/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) -- [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md) -- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md) -- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/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) -- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md) -- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) -- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/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) -- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/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) -- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md) -- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md) -- [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md) -- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md) -- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md) -- [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md) -- [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/index.html.md) -- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/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) -- [updateLineItemsStepWithSelector](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStepWithSelector/index.html.md) -- [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/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) -- [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) +- [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) - [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) +- [notifyOnFailureStep](https://docs.medusajs.com/references/medusa-workflows/steps/notifyOnFailureStep/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) +- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/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) -- [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md) +- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/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) - [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md) -- [cancelOrderClaimStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderClaimStep/index.html.md) +- [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md) - [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/index.html.md) -- [createOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderChangeStep/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) +- [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/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) - [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/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) - [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) -- [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) - [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/index.html.md) - [deleteExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteExchangesStep/index.html.md) @@ -26426,34 +26428,34 @@ For each product variant, you: - [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md) - [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/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) -- [registerOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderFulfillmentStep/index.html.md) +- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/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) - [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/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) - [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md) +- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md) - [updateOrderShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsStep/index.html.md) -- [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/index.html.md) -- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) - [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/index.html.md) -- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md) +- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) - [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/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) -- [createRefundReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRefundReasonStep/index.html.md) - [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md) -- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/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) - [createPriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListPricesStep/index.html.md) - [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/index.html.md) -- [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/index.html.md) - [deletePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePriceListsStep/index.html.md) +- [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/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) +- [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md) - [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md) - [createPricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPricePreferencesStep/index.html.md) - [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md) @@ -26461,12 +26463,12 @@ For each product variant, you: - [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/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) -- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/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) +- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/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) - [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/index.html.md) +- [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/index.html.md) - [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md) - [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md) - [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md) @@ -26477,29 +26479,21 @@ For each product variant, you: - [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) -- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md) - [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md) +- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md) - [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md) -- [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/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) - [groupProductsForBatchStep](https://docs.medusajs.com/references/medusa-workflows/steps/groupProductsForBatchStep/index.html.md) +- [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md) +- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/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) -- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/index.html.md) - [deleteProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductCategoriesStep/index.html.md) -- [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/index.html.md) -- [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md) -- [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) -- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md) -- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md) -- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md) +- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/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) @@ -26511,37 +26505,45 @@ For each product variant, you: - [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md) - [updatePromotionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionRulesStep/index.html.md) - [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md) +- [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/index.html.md) +- [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md) +- [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) +- [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) +- [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) - [deleteReturnReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnReasonStep/index.html.md) - [updateReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnReasonsStep/index.html.md) - [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/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) +- [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md) - [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md) -- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/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) -- [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md) +- [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/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) +- [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md) +- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/index.html.md) - [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/index.html.md) -- [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/index.html.md) +- [deleteShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingProfilesStep/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) -- [updateStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStoresStep/index.html.md) - [deleteStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStoresStep/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) - [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/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) -- [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md) - [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md) +- [deleteTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRateRulesStep/index.html.md) - [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/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) -- [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) - [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/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) - [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) @@ -26690,22 +26692,6 @@ 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.| -# 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 \<host>\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| - - # build Command - Medusa CLI Reference Create a standalone build of the Medusa application. @@ -26768,33 +26754,20 @@ By default, the Medusa Admin is built to the `.medusa/server/public/admin` direc 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. -# new Command - Medusa CLI Reference +# develop 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. +Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. ```bash -medusa new [<dir_name> [<starter_url>]] +npx medusa develop ``` -## Arguments +## Options -|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 \<user>\`|The database user to use for database setup.| -|\`--db-database \<database>\`|The name of the database used for database setup.| -|\`--db-pass \<password>\`|The database password to use for database setup.| -|\`--db-port \<port>\`|The database port to use for database setup.| -|\`--db-host \<host>\`|The database host to use for database setup.| +|Option|Description|Default| +|---|---|---|---|---| +|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| # plugin Commands - Medusa CLI Reference @@ -26858,6 +26831,35 @@ npx medusa plugin:build ``` +# 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 [<dir_name> [<starter_url>]] +``` + +## 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 \<user>\`|The database user to use for database setup.| +|\`--db-database \<database>\`|The name of the database used for database setup.| +|\`--db-pass \<password>\`|The database password to use for database setup.| +|\`--db-port \<port>\`|The database port to use for database setup.| +|\`--db-host \<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). @@ -26874,37 +26876,37 @@ npx medusa exec [file] [args...] |\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| -# 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). +# start 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. +Start the Medusa application in production. ```bash -npx medusa start-cluster +npx medusa start ``` -#### Options +## Options |Option|Description|Default| |---|---|---|---|---| -|\`-c \<number>\`|The number of CPUs that Medusa can consume.|Medusa will try to consume all CPUs.| |\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`| |\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| -# 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 \<number>\`|The number of CPUs that Medusa can consume.|Medusa will try to consume all CPUs.| |\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`| |\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| @@ -26967,68 +26969,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. - - # db Commands - Medusa CLI Reference Commands starting with `db:` perform actions on the database. @@ -27149,36 +27089,66 @@ 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.| -# develop Command - Medusa CLI Reference +# build 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. +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 develop +npx medusa build ``` +Refer to [this section](#run-built-medusa-application) for next steps. + ## Options -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| +|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 | +*** -# exec Command - Medusa CLI Reference +## Run Built Medusa Application -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). +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 -npx medusa exec [file] [args...] +NODE_ENV=production ``` -## Arguments +- Use the `start` command to run the application: -|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| +```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. # new Command - Medusa CLI Reference @@ -27287,6 +27257,22 @@ npx medusa start |\`-p \<port>\`|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| + + # 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). @@ -27306,6 +27292,22 @@ npx medusa start-cluster |\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| +# 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 \<host>\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \<port>\`|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. @@ -27641,314 +27643,314 @@ The object or class passed to `auth.storage` configuration must have the followi ## JS SDK Admin -- [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) +- [batchSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.batchSalesChannels/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) -- [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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/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/Campaign/methods/js_sdk.admin.Campaign.retrieve/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) +- [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) +- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md) +- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md) +- [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md) +- [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md) +- [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/index.html.md) +- [clearToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken/index.html.md) +- [getTokenStorageInfo\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getTokenStorageInfo_/index.html.md) +- [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/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) +- [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) - [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md) +- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md) - [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/index.html.md) - [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md) -- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md) -- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md) - [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md) - [cancel](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancel/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md) -- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md) - [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/index.html.md) +- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md) - [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeInboundItem/index.html.md) - [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md) - [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/index.html.md) -- [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/index.html.md) +- [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/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) -- [clearToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken/index.html.md) - [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md) -- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md) -- [clearToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken_/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) -- [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) -- [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/index.html.md) -- [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/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) -- [throwError\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.throwError_/index.html.md) -- [setToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken_/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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.create/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.delete/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md) +- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md) +- [getItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.getItem/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/DraftOrder/methods/js_sdk.admin.DraftOrder.create/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) - [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.list/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.update/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.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) -- [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) -- [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) - [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/index.html.md) +- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/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) -- [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) - [cancel](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancel/index.html.md) +- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md) - [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/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) - [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/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) - [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/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) - [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md) -- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md) - [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md) -- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.createShipment/index.html.md) +- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/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) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md) - [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md) - [updateServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.updateServiceZone/index.html.md) - [batchInventoryItemLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemLocationLevels/index.html.md) -- [batchInventoryItemsLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemsLocationLevels/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) - [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) - [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md) - [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/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) -- [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/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) +- [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/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/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) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/index.html.md) - [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md) +- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md) +- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md) +- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md) - [cancel](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancel/index.html.md) +- [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/index.html.md) - [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md) -- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md) - [createFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createFulfillment/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) - [list](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.list/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) - [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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrieve/index.html.md) - [requestTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.requestTransfer/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.update/index.html.md) - [retrievePreview](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrievePreview/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.update/index.html.md) +- [batchPrices](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.batchPrices/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/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) +- [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/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) +- [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/index.html.md) +- [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/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) -- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/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) -- [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) - [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) -- [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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md) -- [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/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/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/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/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/index.html.md) +- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md) - [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/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) +- [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.create/index.html.md) -- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md) -- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/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) -- [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md) +- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md) - [export](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.export/index.html.md) +- [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md) - [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/index.html.md) - [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md) - [listVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listVariants/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md) +- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/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) - [updateOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateOption/index.html.md) -- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/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/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/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/ProductCategory/methods/js_sdk.admin.ProductCategory.create/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/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md) +- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/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/ProductCollection/methods/js_sdk.admin.ProductCollection.create/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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.create/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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/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) - [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/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/ProductType/methods/js_sdk.admin.ProductType.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/ProductCategory/methods/js_sdk.admin.ProductCategory.create/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) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/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) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/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) - [create](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.create/index.html.md) -- [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md) +- [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/index.html.md) - [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/index.html.md) - [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/index.html.md) - [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/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) - [updateRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.updateRules/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.retrieve/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/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.create/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/Reservation/methods/js_sdk.admin.Reservation.create/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.update/index.html.md) - [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md) - [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/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) - [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelRequest/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) - [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md) +- [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/index.html.md) +- [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/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) - [removeDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeDismissItem/index.html.md) -- [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md) -- [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md) -- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.retrieve/index.html.md) +- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md) - [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/index.html.md) +- [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/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/ShippingOption/methods/js_sdk.admin.ShippingOption.create/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) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/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/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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/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) -- [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) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/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) - [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/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) -- [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/ShippingOption/methods/js_sdk.admin.ShippingOption.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.retrieve/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) +- [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) +- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md) -- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md) +- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md) - [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md) -- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.create/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.delete/index.html.md) +- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.update/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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/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/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/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) -- [me](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.me/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.list/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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.retrieve/index.html.md) ## JS SDK Auth - [callback](https://docs.medusajs.com/references/js-sdk/auth/callback/index.html.md) +- [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/index.html.md) - [register](https://docs.medusajs.com/references/js-sdk/auth/register/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) -- [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md) - [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md) -- [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/index.html.md) +- [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md) ## JS SDK Store @@ -27956,12 +27958,12 @@ The object or class passed to `auth.storage` configuration must have the followi - [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) -- [order](https://docs.medusajs.com/references/js-sdk/store/order/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) -- [region](https://docs.medusajs.com/references/js-sdk/store/region/index.html.md) +- [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md) +- [order](https://docs.medusajs.com/references/js-sdk/store/order/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) # Configure Medusa Backend @@ -28993,493 +28995,6 @@ 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. - - - -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<HttpTypes.AdminProduct>() - -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 ( - <Badge color={status === "published" ? "green" : "grey"} size="xsmall"> - {status === "published" ? "Published" : "Draft"} - </Badge> - ) - }, - }), -] -``` - -`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<HttpTypes.AdminProduct>() - -const filters = [ - filterHelper.accessor("status", { - type: "select", - label: "Status", - options: [ - { - label: "Published", - value: "published", - }, - { - label: "Draft", - value: "draft", - }, - ], - }), -] -``` - -`createDataTableFilterHelper` utility creates a filter helper that helps you define the filters for the data table. The filter helper has an `accessor` method that accepts two parameters: - -1. The key of a column in the table's data. -2. An object with the following properties: - - `type`: The type of filter. It can be either: - - `select`: A select dropdown allowing users to choose multiple values. - - `radio`: A radio button allowing users to choose one value. - - `date`: A date picker allowing users to choose a date. - - `label`: The filter's label. - - `options`: An array of objects with `label` and `value` properties. The `label` is the option's label, and the `value` is the value to filter by. - -You'll now start creating the UI widget's component. Start by adding the necessary state variables: - -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { - // ... - DataTablePaginationState, - DataTableFilteringState, - DataTableSortingState, -} from "@medusajs/ui" -import { useMemo, useState } from "react" - -// ... - -const limit = 15 - -const CustomPage = () => { - const [pagination, setPagination] = useState<DataTablePaginationState>({ - pageSize: limit, - pageIndex: 0, - }) - const [search, setSearch] = useState<string>("") - const [filtering, setFiltering] = useState<DataTableFilteringState>({}) - const [sorting, setSorting] = useState<DataTableSortingState | null>(null) - - const offset = useMemo(() => { - return pagination.pageIndex * limit - }, [pagination]) - const statusFilters = useMemo(() => { - return (filtering.status || []) as ProductStatus - }, [filtering]) - - // TODO add data fetching logic -} -``` - -In the component, you've added the following state variables: - -- `pagination`: An object of type `DataTablePaginationState` that holds the pagination state. It has two properties: - - `pageSize`: The number of items to show per page. - - `pageIndex`: The current page index. -- `search`: A string that holds the search query. -- `filtering`: An object of type `DataTableFilteringState` that holds the filtering state. -- `sorting`: An object of type `DataTableSortingState` that holds the sorting state. - -You've also added two memoized variables: - -- `offset`: How many items to skip when fetching data based on the current page. -- `statusFilters`: The selected status filters, if any. - -Next, you'll fetch the products from the Medusa application. Assuming you have the JS SDK configured as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), add the following imports at the top of the file: - -```tsx title="src/admin/routes/custom/page.tsx" -import { sdk } from "../../lib/config" -import { useQuery } from "@tanstack/react-query" -``` - -This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest). - -Then, replace the `TODO` in the component with the following: - -```tsx title="src/admin/routes/custom/page.tsx" -const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list({ - limit, - offset, - q: search, - status: statusFilters, - order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, - }), - queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], -}) - -// TODO configure data table -``` - -You use the `useQuery` hook to fetch the products from the Medusa application. In the `queryFn`, you call the `sdk.admin.product.list` method to fetch the products. You pass the following query parameters to the method: - -- `limit`: The number of products to fetch per page. -- `offset`: The number of products to skip based on the current page. -- `q`: The search query, if set. -- `status`: The status filters, if set. -- `order`: The sorting order, if set. - -So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters. - -Next, you'll configure the data table. Medusa UI provides a `useDataTable` hook that helps you configure the data table. Add the following imports at the top of the file: - -```tsx title="src/admin/routes/custom/page.tsx" -import { - // ... - useDataTable, -} from "@medusajs/ui" -import { useNavigate } from "react-router-dom" -``` - -Then, replace the `TODO` in the component with the following: - -```tsx title="src/admin/routes/custom/page.tsx" -const navigate = useNavigate() - -const table = useDataTable({ - columns, - data: data?.products || [], - getRowId: (row) => row.id, - rowCount: data?.count || 0, - isLoading, - pagination: { - state: pagination, - onPaginationChange: setPagination, - }, - search: { - state: search, - onSearchChange: setSearch, - }, - filtering: { - state: filtering, - onFilteringChange: setFiltering, - }, - filters, - sorting: { - // Pass the pagination state and updater to the table instance - state: sorting, - onSortingChange: setSorting, - }, - onRowClick: (event, row) => { - // Handle row click, for example - navigate(`/products/${row.id}`) - }, -}) - -// TODO render component -``` - -The `useDataTable` hook accepts an object with the following properties: - -- columns: (\`array\`) The columns to display in the data table. You created this using the \`createDataTableColumnHelper\` utility. -- data: (\`array\`) The products fetched from the Medusa application. -- getRowId: (\`function\`) A function that returns the unique ID of a row. -- rowCount: (\`number\`) The total number of products that can be retrieved. This is used to determine the number of pages. -- isLoading: (\`boolean\`) A boolean that indicates if the data is being fetched. -- pagination: (\`object\`) An object to configure pagination. - - - state: (\`object\`) The pagination React state variable. - - - onPaginationChange: (\`function\`) A function that updates the pagination state. -- search: (\`object\`) An object to configure searching. - - - state: (\`string\`) The search query React state variable. - - - onSearchChange: (\`function\`) A function that updates the search query state. -- filtering: (\`object\`) An object to configure filtering. - - - state: (\`object\`) The filtering React state variable. - - - onFilteringChange: (\`function\`) A function that updates the filtering state. -- filters: (\`array\`) The filters to display in the data table. You created this using the \`createDataTableFilterHelper\` utility. -- sorting: (\`object\`) An object to configure sorting. - - - state: (\`object\`) The sorting React state variable. - - - onSortingChange: (\`function\`) A function that updates the sorting state. -- onRowClick: (\`function\`) A function that allows you to perform an action when the user clicks on a row. In this example, you navigate to the product's detail page. - - - event: (\`mouseevent\`) An instance of the \[MouseClickEvent]\(https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object. - - - row: (\`object\`) The data of the row that was clicked. - -Finally, you'll render the data table. But first, add the following imports at the top of the page: - -```tsx title="src/admin/routes/custom/page.tsx" -import { - // ... - DataTable, -} from "@medusajs/ui" -import { SingleColumnLayout } from "../../layouts/single-column" -import { Container } from "../../components/container" -``` - -Aside from the `DataTable` component, you also import the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md) and [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard. - -Then, replace the `TODO` in the component with the following: - -```tsx title="src/admin/routes/custom/page.tsx" -return ( - <SingleColumnLayout> - <Container> - <DataTable instance={table}> - <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center"> - <Heading>Products</Heading> - <div className="flex gap-2"> - <DataTable.FilterMenu tooltip="Filter" /> - <DataTable.SortingMenu tooltip="Sort" /> - <DataTable.Search placeholder="Search..." /> - </div> - </DataTable.Toolbar> - <DataTable.Table /> - <DataTable.Pagination /> - </DataTable> - </Container> - </SingleColumnLayout> -) -``` - -You render the `DataTable` component and pass the `table` instance as a prop. In the `DataTable` component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table. - -Lastly, export the component and the UI widget's configuration at the end of the file: - -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" - -// ... - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - -If you start your Medusa application and go to `localhost:9000/app/custom`, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities. - -### Full Example Code - -```tsx title="src/admin/routes/custom/page.tsx" -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" -import { - Badge, - createDataTableColumnHelper, - createDataTableFilterHelper, - DataTable, - DataTableFilteringState, - DataTablePaginationState, - DataTableSortingState, - Heading, - useDataTable, -} from "@medusajs/ui" -import { useQuery } from "@tanstack/react-query" -import { SingleColumnLayout } from "../../layouts/single-column" -import { sdk } from "../../lib/config" -import { useMemo, useState } from "react" -import { Container } from "../../components/container" -import { HttpTypes, ProductStatus } from "@medusajs/framework/types" - -const columnHelper = createDataTableColumnHelper<HttpTypes.AdminProduct>() - -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 ( - <Badge color={status === "published" ? "green" : "grey"} size="xsmall"> - {status === "published" ? "Published" : "Draft"} - </Badge> - ) - }, - }), -] - -const filterHelper = createDataTableFilterHelper<HttpTypes.AdminProduct>() - -const filters = [ - filterHelper.accessor("status", { - type: "select", - label: "Status", - options: [ - { - label: "Published", - value: "published", - }, - { - label: "Draft", - value: "draft", - }, - ], - }), -] - -const limit = 15 - -const CustomPage = () => { - const [pagination, setPagination] = useState<DataTablePaginationState>({ - pageSize: limit, - pageIndex: 0, - }) - const [search, setSearch] = useState<string>("") - const [filtering, setFiltering] = useState<DataTableFilteringState>({}) - const [sorting, setSorting] = useState<DataTableSortingState | null>(null) - - const offset = useMemo(() => { - return pagination.pageIndex * limit - }, [pagination]) - const statusFilters = useMemo(() => { - return (filtering.status || []) as ProductStatus - }, [filtering]) - - const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list({ - limit, - offset, - q: search, - status: statusFilters, - order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, - }), - queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], - }) - - const table = useDataTable({ - columns, - data: data?.products || [], - getRowId: (row) => row.id, - rowCount: data?.count || 0, - isLoading, - pagination: { - state: pagination, - onPaginationChange: setPagination, - }, - search: { - state: search, - onSearchChange: setSearch, - }, - filtering: { - state: filtering, - onFilteringChange: setFiltering, - }, - filters, - sorting: { - // Pass the pagination state and updater to the table instance - state: sorting, - onSortingChange: setSorting, - }, - }) - - return ( - <SingleColumnLayout> - <Container> - <DataTable instance={table}> - <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center"> - <Heading>Products</Heading> - <div className="flex gap-2"> - <DataTable.FilterMenu tooltip="Filter" /> - <DataTable.SortingMenu tooltip="Sort" /> - <DataTable.Search placeholder="Search..." /> - </div> - </DataTable.Toolbar> - <DataTable.Table /> - <DataTable.Pagination /> - </DataTable> - </Container> - </SingleColumnLayout> - ) -} - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - - # Forms - Admin Components The Medusa Admin has two types of forms: @@ -30053,427 +29568,686 @@ This component uses the [Container](https://docs.medusajs.com/Users/shahednasser 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 +# Data Table - Admin Components -Each section in the Medusa Admin has a header with a title, and optionally a subtitle with buttons to perform an action. +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. -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" +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. -export type HeadingProps = { - title: string - subtitle?: string - actions?: ( - { - type: "button", - props: React.ComponentProps<typeof Button> - link?: LinkProps - } | - { - type: "action-menu" - props: ActionMenuProps - } | - { - type: "custom" - children: React.ReactNode - } - )[] -} +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. -export const Header = ({ - title, - subtitle, - actions = [], -}: HeadingProps) => { - return ( - <div className="flex items-center justify-between px-6 py-4"> - <div> - <Heading level="h2">{title}</Heading> - {subtitle && ( - <Text className="text-ui-fg-subtle" size="small"> - {subtitle} - </Text> - )} - </div> - {actions.length > 0 && ( - <div className="flex items-center justify-center gap-x-2"> - {actions.map((action, index) => ( - <> - {action.type === "button" && ( - <Button - {...action.props} - size={action.props.size || "small"} - key={index} - > - <> - {action.props.children} - {action.link && <Link {...action.link} />} - </> - </Button> - )} - {action.type === "action-menu" && ( - <ActionMenu {...action.props} /> - )} - {action.type === "custom" && action.children} - </> - ))} - </div> - )} - </div> - ) -} +## 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<HttpTypes.AdminProduct>() + +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 ( + <Badge color={status === "published" ? "green" : "grey"} size="xsmall"> + {status === "published" ? "Published" : "Draft"} + </Badge> + ) + }, + }), +] ``` -The `Header` component shows a title, and optionally a subtitle and action buttons. +`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: -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. 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". -It accepts the following props: +Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status. -- title: (\`string\`) The section's title. -- subtitle: (\`string\`) The section's subtitle. -- actions: (\`object\[]\`) An array of actions to show. +To define the filters, add the following: - - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add. +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { + // ... + createDataTableFilterHelper, +} from "@medusajs/ui" - \- If its value is \`button\`, it'll show a button that can have a link or an on-click action. +const filterHelper = createDataTableFilterHelper<HttpTypes.AdminProduct>() - \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions. +const filters = [ + filterHelper.accessor("status", { + type: "select", + label: "Status", + options: [ + { + label: "Published", + value: "published", + }, + { + label: "Draft", + value: "draft", + }, + ], + }), +] +``` - \- If its value is \`custom\`, you can pass any React nodes to render. +`createDataTableFilterHelper` utility creates a filter helper that helps you define the filters for the data table. The filter helper has an `accessor` method that accepts two parameters: - - props: (object) +1. The key of a column in the table's data. +2. An object with the following properties: + - `type`: The type of filter. It can be either: + - `select`: A select dropdown allowing users to choose multiple values. + - `radio`: A radio button allowing users to choose one value. + - `date`: A date picker allowing users to choose a date. + - `label`: The filter's label. + - `options`: An array of objects with `label` and `value` properties. The `label` is the option's label, and the `value` is the value to filter by. - - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions. +You'll now start creating the UI widget's component. Start by adding the necessary state variables: -*** +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { + // ... + DataTablePaginationState, + DataTableFilteringState, + DataTableSortingState, +} from "@medusajs/ui" +import { useMemo, useState } from "react" -## Example +// ... -Use the `Header` component in any widget or UI route. +const limit = 15 -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: +const CustomPage = () => { + const [pagination, setPagination] = useState<DataTablePaginationState>({ + pageSize: limit, + pageIndex: 0, + }) + const [search, setSearch] = useState<string>("") + const [filtering, setFiltering] = useState<DataTableFilteringState>({}) + const [sorting, setSorting] = useState<DataTableSortingState | null>(null) -```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 offset = useMemo(() => { + return pagination.pageIndex * limit + }, [pagination]) + const statusFilters = useMemo(() => { + return (filtering.status || []) as ProductStatus + }, [filtering]) -const ProductWidget = () => { - return ( - <Container> - <Header - title="Product Widget" - subtitle="This is my custom product widget" - actions={[ - { - type: "button", - props: { - children: "Click me", - variant: "secondary", - onClick: () => { - alert("You clicked the button.") - }, - }, - }, - ]} - /> - </Container> - ) + // TODO add data fetching logic } +``` -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) +In the component, you've added the following state variables: -export default ProductWidget +- `pagination`: An object of type `DataTablePaginationState` that holds the pagination state. It has two properties: + - `pageSize`: The number of items to show per page. + - `pageIndex`: The current page index. +- `search`: A string that holds the search query. +- `filtering`: An object of type `DataTableFilteringState` that holds the filtering state. +- `sorting`: An object of type `DataTableSortingState` that holds the sorting state. + +You've also added two memoized variables: + +- `offset`: How many items to skip when fetching data based on the current page. +- `statusFilters`: The selected status filters, if any. + +Next, you'll fetch the products from the Medusa application. Assuming you have the JS SDK configured as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), add the following imports at the top of the file: + +```tsx title="src/admin/routes/custom/page.tsx" +import { sdk } from "../../lib/config" +import { useQuery } from "@tanstack/react-query" ``` -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. +This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest). + +Then, replace the `TODO` in the component with the following: +```tsx title="src/admin/routes/custom/page.tsx" +const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list({ + limit, + offset, + q: search, + status: statusFilters, + order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, + }), + queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], +}) -# Container - Admin Components +// TODO configure data table +``` -The Medusa Admin wraps each section of a page in a container. +You use the `useQuery` hook to fetch the products from the Medusa application. In the `queryFn`, you call the `sdk.admin.product.list` method to fetch the products. You pass the following query parameters to the method: - +- `limit`: The number of products to fetch per page. +- `offset`: The number of products to skip based on the current page. +- `q`: The search query, if set. +- `status`: The status filters, if set. +- `order`: The sorting order, if set. -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: +So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters. -```tsx -import { - Container as UiContainer, - clx, +Next, you'll configure the data table. Medusa UI provides a `useDataTable` hook that helps you configure the data table. Add the following imports at the top of the file: + +```tsx title="src/admin/routes/custom/page.tsx" +import { + // ... + useDataTable, } from "@medusajs/ui" +import { useNavigate } from "react-router-dom" +``` -type ContainerProps = React.ComponentProps<typeof UiContainer> +Then, replace the `TODO` in the component with the following: -export const Container = (props: ContainerProps) => { - return ( - <UiContainer {...props} className={clx( - "divide-y p-0", - props.className - )} /> - ) -} +```tsx title="src/admin/routes/custom/page.tsx" +const navigate = useNavigate() + +const table = useDataTable({ + columns, + data: data?.products || [], + getRowId: (row) => row.id, + rowCount: data?.count || 0, + isLoading, + pagination: { + state: pagination, + onPaginationChange: setPagination, + }, + search: { + state: search, + onSearchChange: setSearch, + }, + filtering: { + state: filtering, + onFilteringChange: setFiltering, + }, + filters, + sorting: { + // Pass the pagination state and updater to the table instance + state: sorting, + onSortingChange: setSorting, + }, + onRowClick: (event, row) => { + // Handle row click, for example + navigate(`/products/${row.id}`) + }, +}) + +// TODO render component ``` -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 `useDataTable` hook accepts an object with the following properties: -*** +- columns: (\`array\`) The columns to display in the data table. You created this using the \`createDataTableColumnHelper\` utility. +- data: (\`array\`) The products fetched from the Medusa application. +- getRowId: (\`function\`) A function that returns the unique ID of a row. +- rowCount: (\`number\`) The total number of products that can be retrieved. This is used to determine the number of pages. +- isLoading: (\`boolean\`) A boolean that indicates if the data is being fetched. +- pagination: (\`object\`) An object to configure pagination. -## Example + - state: (\`object\`) The pagination React state variable. -Use that `Container` component in any widget or UI route. + - onPaginationChange: (\`function\`) A function that updates the pagination state. +- search: (\`object\`) An object to configure searching. -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: + - state: (\`string\`) The search query React state variable. -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container } from "../components/container" -import { Header } from "../components/header" + - onSearchChange: (\`function\`) A function that updates the search query state. +- filtering: (\`object\`) An object to configure filtering. -const ProductWidget = () => { - return ( + - state: (\`object\`) The filtering React state variable. + + - onFilteringChange: (\`function\`) A function that updates the filtering state. +- filters: (\`array\`) The filters to display in the data table. You created this using the \`createDataTableFilterHelper\` utility. +- sorting: (\`object\`) An object to configure sorting. + + - state: (\`object\`) The sorting React state variable. + + - onSortingChange: (\`function\`) A function that updates the sorting state. +- onRowClick: (\`function\`) A function that allows you to perform an action when the user clicks on a row. In this example, you navigate to the product's detail page. + + - event: (\`mouseevent\`) An instance of the \[MouseClickEvent]\(https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object. + + - row: (\`object\`) The data of the row that was clicked. + +Finally, you'll render the data table. But first, add the following imports at the top of the page: + +```tsx title="src/admin/routes/custom/page.tsx" +import { + // ... + DataTable, +} from "@medusajs/ui" +import { SingleColumnLayout } from "../../layouts/single-column" +import { Container } from "../../components/container" +``` + +Aside from the `DataTable` component, you also import the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md) and [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard. + +Then, replace the `TODO` in the component with the following: + +```tsx title="src/admin/routes/custom/page.tsx" +return ( + <SingleColumnLayout> <Container> - <Header title="Product Widget" /> + <DataTable instance={table}> + <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center"> + <Heading>Products</Heading> + <div className="flex gap-2"> + <DataTable.FilterMenu tooltip="Filter" /> + <DataTable.SortingMenu tooltip="Sort" /> + <DataTable.Search placeholder="Search..." /> + </div> + </DataTable.Toolbar> + <DataTable.Table /> + <DataTable.Pagination /> + </DataTable> </Container> - ) -} + </SingleColumnLayout> +) +``` -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) +You render the `DataTable` component and pass the `table` instance as a prop. In the `DataTable` component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table. -export default ProductWidget -``` +Lastly, export the component and the UI widget's configuration at the end of the file: -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. +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" +// ... -# JSON View - Admin Components +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) -Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format. +export default CustomPage +``` - +If you start your Medusa application and go to `localhost:9000/app/custom`, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities. -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: +### Full Example Code -```tsx title="src/admin/components/json-view-section.tsx" -import { - ArrowUpRightOnBox, - Check, - SquareTwoStack, - TriangleDownMini, - XMarkMini, -} from "@medusajs/icons" -import { +```tsx title="src/admin/routes/custom/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" +import { Badge, - Container, - Drawer, + createDataTableColumnHelper, + createDataTableFilterHelper, + DataTable, + DataTableFilteringState, + DataTablePaginationState, + DataTableSortingState, Heading, - IconButton, - Kbd, + useDataTable, } from "@medusajs/ui" -import Primitive from "@uiw/react-json-view" -import { CSSProperties, MouseEvent, Suspense, useState } from "react" +import { useQuery } from "@tanstack/react-query" +import { SingleColumnLayout } from "../../layouts/single-column" +import { sdk } from "../../lib/config" +import { useMemo, useState } from "react" +import { Container } from "../../components/container" +import { HttpTypes, ProductStatus } from "@medusajs/framework/types" -type JsonViewSectionProps = { - data: object - title?: string -} +const columnHelper = createDataTableColumnHelper<HttpTypes.AdminProduct>() -export const JsonViewSection = ({ data }: JsonViewSectionProps) => { - const numberOfKeys = Object.keys(data).length +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 ( + <Badge color={status === "published" ? "green" : "grey"} size="xsmall"> + {status === "published" ? "Published" : "Draft"} + </Badge> + ) + }, + }), +] + +const filterHelper = createDataTableFilterHelper<HttpTypes.AdminProduct>() + +const filters = [ + filterHelper.accessor("status", { + type: "select", + label: "Status", + options: [ + { + label: "Published", + value: "published", + }, + { + label: "Draft", + value: "draft", + }, + ], + }), +] + +const limit = 15 + +const CustomPage = () => { + const [pagination, setPagination] = useState<DataTablePaginationState>({ + pageSize: limit, + pageIndex: 0, + }) + const [search, setSearch] = useState<string>("") + const [filtering, setFiltering] = useState<DataTableFilteringState>({}) + const [sorting, setSorting] = useState<DataTableSortingState | null>(null) + + const offset = useMemo(() => { + return pagination.pageIndex * limit + }, [pagination]) + const statusFilters = useMemo(() => { + return (filtering.status || []) as ProductStatus + }, [filtering]) + + const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list({ + limit, + offset, + q: search, + status: statusFilters, + order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, + }), + queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], + }) + + const table = useDataTable({ + columns, + data: data?.products || [], + getRowId: (row) => row.id, + rowCount: data?.count || 0, + isLoading, + pagination: { + state: pagination, + onPaginationChange: setPagination, + }, + search: { + state: search, + onSearchChange: setSearch, + }, + filtering: { + state: filtering, + onFilteringChange: setFiltering, + }, + filters, + sorting: { + // Pass the pagination state and updater to the table instance + state: sorting, + onSortingChange: setSorting, + }, + }) return ( - <Container className="flex items-center justify-between px-6 py-4"> - <div className="flex items-center gap-x-4"> - <Heading level="h2">JSON</Heading> - <Badge size="2xsmall" rounded="full"> - {numberOfKeys} keys - </Badge> - </div> - <Drawer> - <Drawer.Trigger asChild> - <IconButton - size="small" - variant="transparent" - className="text-ui-fg-muted hover:text-ui-fg-subtle" - > - <ArrowUpRightOnBox /> - </IconButton> - </Drawer.Trigger> - <Drawer.Content className="bg-ui-contrast-bg-base text-ui-code-fg-subtle !shadow-elevation-commandbar overflow-hidden border border-none max-md:inset-x-2 max-md:max-w-[calc(100%-16px)]"> - <div className="bg-ui-code-bg-base flex items-center justify-between px-6 py-4"> - <div className="flex items-center gap-x-4"> - <Drawer.Title asChild> - <Heading className="text-ui-contrast-fg-primary"> - <span className="text-ui-fg-subtle"> - {numberOfKeys} - </span> - </Heading> - </Drawer.Title> - </div> - <div className="flex items-center gap-x-2"> - <Kbd className="bg-ui-contrast-bg-subtle border-ui-contrast-border-base text-ui-contrast-fg-secondary"> - esc - </Kbd> - <Drawer.Close asChild> - <IconButton - size="small" - variant="transparent" - className="text-ui-contrast-fg-secondary hover:text-ui-contrast-fg-primary hover:bg-ui-contrast-bg-base-hover active:bg-ui-contrast-bg-base-pressed focus-visible:bg-ui-contrast-bg-base-hover focus-visible:shadow-borders-interactive-with-active" - > - <XMarkMini /> - </IconButton> - </Drawer.Close> - </div> - </div> - <Drawer.Body className="flex flex-1 flex-col overflow-hidden px-[5px] py-0 pb-[5px]"> - <div className="bg-ui-contrast-bg-subtle flex-1 overflow-auto rounded-b-[4px] rounded-t-lg p-3"> - <Suspense - fallback={<div className="flex size-full flex-col"></div>} - > - <Primitive - value={data} - displayDataTypes={false} - style={ - { - "--w-rjv-font-family": "Roboto Mono, monospace", - "--w-rjv-line-color": "var(--contrast-border-base)", - "--w-rjv-curlybraces-color": - "var(--contrast-fg-secondary)", - "--w-rjv-brackets-color": "var(--contrast-fg-secondary)", - "--w-rjv-key-string": "var(--contrast-fg-primary)", - "--w-rjv-info-color": "var(--contrast-fg-secondary)", - "--w-rjv-type-string-color": "var(--tag-green-icon)", - "--w-rjv-quotes-string-color": "var(--tag-green-icon)", - "--w-rjv-type-boolean-color": "var(--tag-orange-icon)", - "--w-rjv-type-int-color": "var(--tag-orange-icon)", - "--w-rjv-type-float-color": "var(--tag-orange-icon)", - "--w-rjv-type-bigint-color": "var(--tag-orange-icon)", - "--w-rjv-key-number": "var(--contrast-fg-secondary)", - "--w-rjv-arrow-color": "var(--contrast-fg-secondary)", - "--w-rjv-copied-color": "var(--contrast-fg-secondary)", - "--w-rjv-copied-success-color": - "var(--contrast-fg-primary)", - "--w-rjv-colon-color": "var(--contrast-fg-primary)", - "--w-rjv-ellipsis-color": "var(--contrast-fg-secondary)", - } as CSSProperties - } - collapsed={1} - > - <Primitive.Quote render={() => <span />} /> - <Primitive.Null - render={() => ( - <span className="text-ui-tag-red-icon">null</span> - )} - /> - <Primitive.Undefined - render={() => ( - <span className="text-ui-tag-blue-icon">undefined</span> - )} - /> - <Primitive.CountInfo - render={(_props, { value }) => { - return ( - <span className="text-ui-contrast-fg-secondary ml-2"> - {Object.keys(value as object).length} items - </span> - ) - }} - /> - <Primitive.Arrow> - <TriangleDownMini className="text-ui-contrast-fg-secondary -ml-[0.5px]" /> - </Primitive.Arrow> - <Primitive.Colon> - <span className="mr-1">:</span> - </Primitive.Colon> - <Primitive.Copied - render={({ style }, { value }) => { - return <Copied style={style} value={value} /> - }} - /> - </Primitive> - </Suspense> + <SingleColumnLayout> + <Container> + <DataTable instance={table}> + <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center"> + <Heading>Products</Heading> + <div className="flex gap-2"> + <DataTable.FilterMenu tooltip="Filter" /> + <DataTable.SortingMenu tooltip="Sort" /> + <DataTable.Search placeholder="Search..." /> </div> - </Drawer.Body> - </Drawer.Content> - </Drawer> - </Container> + </DataTable.Toolbar> + <DataTable.Table /> + <DataTable.Pagination /> + </DataTable> + </Container> + </SingleColumnLayout> + ) +} + +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) + +export default CustomPage +``` + + +# 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. + + + +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" + +export type HeadingProps = { + title: string + subtitle?: string + actions?: ( + { + type: "button", + props: React.ComponentProps<typeof Button> + link?: LinkProps + } | + { + type: "action-menu" + props: ActionMenuProps + } | + { + type: "custom" + children: React.ReactNode + } + )[] +} + +export const Header = ({ + title, + subtitle, + actions = [], +}: HeadingProps) => { + return ( + <div className="flex items-center justify-between px-6 py-4"> + <div> + <Heading level="h2">{title}</Heading> + {subtitle && ( + <Text className="text-ui-fg-subtle" size="small"> + {subtitle} + </Text> + )} + </div> + {actions.length > 0 && ( + <div className="flex items-center justify-center gap-x-2"> + {actions.map((action, index) => ( + <> + {action.type === "button" && ( + <Button + {...action.props} + size={action.props.size || "small"} + key={index} + > + <> + {action.props.children} + {action.link && <Link {...action.link} />} + </> + </Button> + )} + {action.type === "action-menu" && ( + <ActionMenu {...action.props} /> + )} + {action.type === "custom" && action.children} + </> + ))} + </div> + )} + </div> ) } +``` -type CopiedProps = { - style?: CSSProperties - value: object | undefined -} +The `Header` component shows a title, and optionally a subtitle and action buttons. -const Copied = ({ style, value }: CopiedProps) => { - const [copied, setCopied] = useState(false) +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. - const handler = (e: MouseEvent<HTMLSpanElement>) => { - e.stopPropagation() - setCopied(true) +It accepts the following props: - if (typeof value === "string") { - navigator.clipboard.writeText(value) - } else { - const json = JSON.stringify(value, null, 2) - navigator.clipboard.writeText(json) - } +- title: (\`string\`) The section's title. +- subtitle: (\`string\`) The section's subtitle. +- actions: (\`object\[]\`) An array of actions to show. - setTimeout(() => { - setCopied(false) - }, 2000) - } + - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add. - const styl = { whiteSpace: "nowrap", width: "20px" } + \- If its value is \`button\`, it'll show a button that can have a link or an on-click action. - if (copied) { - return ( - <span style={{ ...style, ...styl }}> - <Check className="text-ui-contrast-fg-primary" /> - </span> - ) - } + \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions. + + \- If its value is \`custom\`, you can pass any React nodes to render. + + - props: (object) + + - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions. + +*** + +## Example + +Use the `Header` 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 { Container } from "../components/container" +import { Header } from "../components/header" +const ProductWidget = () => { return ( - <span style={{ ...style, ...styl }} onClick={handler}> - <SquareTwoStack className="text-ui-contrast-fg-secondary" /> - </span> + <Container> + <Header + title="Product Widget" + subtitle="This is my custom product widget" + actions={[ + { + type: "button", + props: { + children: "Click me", + variant: "secondary", + onClick: () => { + alert("You clicked the button.") + }, + }, + }, + ]} + /> + </Container> ) } + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget ``` -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. +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. -The `JsonViewSection` accepts a `data` prop, which is the data to show as a JSON object in the drawer. + +# Container - Admin Components + +The Medusa Admin wraps each section of a page in a container. + + + +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 +import { + Container as UiContainer, + clx, +} from "@medusajs/ui" + +type ContainerProps = React.ComponentProps<typeof UiContainer> + +export const Container = (props: ContainerProps) => { + return ( + <UiContainer {...props} className={clx( + "divide-y p-0", + props.className + )} /> + ) +} +``` + +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. *** ## Example -Use the `JsonViewSection` component in any 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: ```tsx title="src/admin/widgets/product-widget.tsx" import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { JsonViewSection } from "../components/json-view-section" +import { Container } from "../components/container" +import { Header } from "../components/header" const ProductWidget = () => { - return <JsonViewSection data={{ - name: "John", - }} /> + return ( + <Container> + <Header title="Product Widget" /> + </Container> + ) } export const config = defineWidgetConfig({ @@ -30483,7 +30257,7 @@ 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" }`. +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. # Section Row - Admin Components @@ -30868,31 +30642,402 @@ If `data` isn't `undefined`, you display the `Table` component passing it the fo To test it out, log into the Medusa Admin and open `http://localhost:9000/app/custom`. You'll find a table of products with pagination. +# JSON View - Admin Components + +Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format. + + + +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/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" + +type JsonViewSectionProps = { + data: object + title?: string +} + +export const JsonViewSection = ({ data }: JsonViewSectionProps) => { + const numberOfKeys = Object.keys(data).length + + return ( + <Container className="flex items-center justify-between px-6 py-4"> + <div className="flex items-center gap-x-4"> + <Heading level="h2">JSON</Heading> + <Badge size="2xsmall" rounded="full"> + {numberOfKeys} keys + </Badge> + </div> + <Drawer> + <Drawer.Trigger asChild> + <IconButton + size="small" + variant="transparent" + className="text-ui-fg-muted hover:text-ui-fg-subtle" + > + <ArrowUpRightOnBox /> + </IconButton> + </Drawer.Trigger> + <Drawer.Content className="bg-ui-contrast-bg-base text-ui-code-fg-subtle !shadow-elevation-commandbar overflow-hidden border border-none max-md:inset-x-2 max-md:max-w-[calc(100%-16px)]"> + <div className="bg-ui-code-bg-base flex items-center justify-between px-6 py-4"> + <div className="flex items-center gap-x-4"> + <Drawer.Title asChild> + <Heading className="text-ui-contrast-fg-primary"> + <span className="text-ui-fg-subtle"> + {numberOfKeys} + </span> + </Heading> + </Drawer.Title> + </div> + <div className="flex items-center gap-x-2"> + <Kbd className="bg-ui-contrast-bg-subtle border-ui-contrast-border-base text-ui-contrast-fg-secondary"> + esc + </Kbd> + <Drawer.Close asChild> + <IconButton + size="small" + variant="transparent" + className="text-ui-contrast-fg-secondary hover:text-ui-contrast-fg-primary hover:bg-ui-contrast-bg-base-hover active:bg-ui-contrast-bg-base-pressed focus-visible:bg-ui-contrast-bg-base-hover focus-visible:shadow-borders-interactive-with-active" + > + <XMarkMini /> + </IconButton> + </Drawer.Close> + </div> + </div> + <Drawer.Body className="flex flex-1 flex-col overflow-hidden px-[5px] py-0 pb-[5px]"> + <div className="bg-ui-contrast-bg-subtle flex-1 overflow-auto rounded-b-[4px] rounded-t-lg p-3"> + <Suspense + fallback={<div className="flex size-full flex-col"></div>} + > + <Primitive + value={data} + displayDataTypes={false} + style={ + { + "--w-rjv-font-family": "Roboto Mono, monospace", + "--w-rjv-line-color": "var(--contrast-border-base)", + "--w-rjv-curlybraces-color": + "var(--contrast-fg-secondary)", + "--w-rjv-brackets-color": "var(--contrast-fg-secondary)", + "--w-rjv-key-string": "var(--contrast-fg-primary)", + "--w-rjv-info-color": "var(--contrast-fg-secondary)", + "--w-rjv-type-string-color": "var(--tag-green-icon)", + "--w-rjv-quotes-string-color": "var(--tag-green-icon)", + "--w-rjv-type-boolean-color": "var(--tag-orange-icon)", + "--w-rjv-type-int-color": "var(--tag-orange-icon)", + "--w-rjv-type-float-color": "var(--tag-orange-icon)", + "--w-rjv-type-bigint-color": "var(--tag-orange-icon)", + "--w-rjv-key-number": "var(--contrast-fg-secondary)", + "--w-rjv-arrow-color": "var(--contrast-fg-secondary)", + "--w-rjv-copied-color": "var(--contrast-fg-secondary)", + "--w-rjv-copied-success-color": + "var(--contrast-fg-primary)", + "--w-rjv-colon-color": "var(--contrast-fg-primary)", + "--w-rjv-ellipsis-color": "var(--contrast-fg-secondary)", + } as CSSProperties + } + collapsed={1} + > + <Primitive.Quote render={() => <span />} /> + <Primitive.Null + render={() => ( + <span className="text-ui-tag-red-icon">null</span> + )} + /> + <Primitive.Undefined + render={() => ( + <span className="text-ui-tag-blue-icon">undefined</span> + )} + /> + <Primitive.CountInfo + render={(_props, { value }) => { + return ( + <span className="text-ui-contrast-fg-secondary ml-2"> + {Object.keys(value as object).length} items + </span> + ) + }} + /> + <Primitive.Arrow> + <TriangleDownMini className="text-ui-contrast-fg-secondary -ml-[0.5px]" /> + </Primitive.Arrow> + <Primitive.Colon> + <span className="mr-1">:</span> + </Primitive.Colon> + <Primitive.Copied + render={({ style }, { value }) => { + return <Copied style={style} value={value} /> + }} + /> + </Primitive> + </Suspense> + </div> + </Drawer.Body> + </Drawer.Content> + </Drawer> + </Container> + ) +} + +type CopiedProps = { + style?: CSSProperties + value: object | undefined +} + +const Copied = ({ style, value }: CopiedProps) => { + const [copied, setCopied] = useState(false) + + const handler = (e: MouseEvent<HTMLSpanElement>) => { + e.stopPropagation() + setCopied(true) + + if (typeof value === "string") { + navigator.clipboard.writeText(value) + } else { + const json = JSON.stringify(value, null, 2) + navigator.clipboard.writeText(json) + } + + setTimeout(() => { + setCopied(false) + }, 2000) + } + + const styl = { whiteSpace: "nowrap", width: "20px" } + + if (copied) { + return ( + <span style={{ ...style, ...styl }}> + <Check className="text-ui-contrast-fg-primary" /> + </span> + ) + } + + return ( + <span style={{ ...style, ...styl }} onClick={handler}> + <SquareTwoStack className="text-ui-contrast-fg-secondary" /> + </span> + ) +} +``` + +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. + +The `JsonViewSection` accepts a `data` prop, which is the data to show as a JSON object in the drawer. + +*** + +## 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 <JsonViewSection data={{ + name: "John", + }} /> +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`. + + # Service Factory Reference This section of the documentation provides a reference of the methods generated for services extending the service factory (`MedusaService`), and how to use them. -Learn more about the service factory in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/service-factory/index.html.md). +Learn more about the service factory in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/service-factory/index.html.md). + +## Method Names + +Generated method names are of the format `{operationName}_{dataModelName}`, where: + +- `{operationName}` is the name of the operation. For example, `create`. +- `{dataModelName}` is the pascal-case version of the data model's key that's passed in the object parameter of `MedusaService`. The name is pluralized for all operations except for the `retrieve` operation. + +Some examples of method names: + +- `createPosts` +- `createMyPosts` +- `retrievePost` +- `listPosts` + +*** + +## Methods Reference + +The reference uses only the operation name to refer to the method. + + +# Filter Records - Service Factory Reference + +Many of the service factory's generated methods allow passing filters to perform an operation, such as to update or delete records matching the filters. + +This guide provides examples of using filters. + +The `list` method is used in the example snippets of this reference, but you can use the same filtering mechanism with any method that accepts filters. + +*** + +## Match Exact Value + +```ts +const posts = await postModuleService.listPosts({ + name: "My Post 2", +}) +``` + +If you pass a property with its value, only records whose properties exactly match the value are selected. + +In the example above, only posts having the name `My Post 2` are retrieved. + +*** + +## Match Multiple Values + +```ts +const posts = await postModuleService.listPosts({ + views: [ + 50, + 100, + ], +}) +``` + +To find records with a property matching multiple values, pass an array of those values as the property's value in the filter. + +In the example above, only posts having either `50` or `100` views are retrieved. + +*** + +## Don't Match Values + +```ts +const posts = await postModuleService.listPosts({ + name: { + $nin: [ + "My Post", + ], + }, +}) +``` + +To find records with a property that doesn't match one or more values, pass an object with a `$nin` property. Its value is an array of multiple values that a record's property shouldn't match. + +In the example above, only posts that don't have the name `My Post` are retrieved. + +*** + +## Match Text Like Value + +This filter only applies to text-like properties, including `id` and `enum` properties. + +```ts +const posts = await postModuleService.listPosts({ + name: { + $like: "My%", + }, +}) +``` + +To perform a `like` filter on a record's property, set the property's value to an object with a `$like` property. Its value is the string to use when applying the `like` filter. + +The example above matches all posts whose name starts with `My`. + +*** + +## Apply Range Filters + +This filter only applies to the `number` and `dateTime` properties. + +```ts +const posts = await postModuleService.listPosts({ + published_at: { + $lt: new Date(), + }, +}) +``` + +To filter a record's property to be within a range, set the property's value to an object with any of the following properties: + +1. `$lt`: The property's value must be less than the supplied value. +2. `$lte`: The property's value must be less than or equal to the supplied value. +3. `$gt`: The property's value must be greater than the supplied value. +4. `$gte`: The property's value must be greater than or equal the supplied value. + +In the example above, only posts whose `published_at` property is before the current date and time are retrieved. + +### Example: Retrieve Posts Published Today + +```ts +const startToday = new Date() +startToday.setHours(0, 0, 0, 0) + +const endToday = new Date() +endToday.setHours(23, 59, 59, 59) -## Method Names +const posts = await postModuleService.listPosts({ + published_at: { + $gte: startToday, + $lte: endToday, + }, +}) +``` -Generated method names are of the format `{operationName}_{dataModelName}`, where: +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. -- `{operationName}` is the name of the operation. For example, `create`. -- `{dataModelName}` is the pascal-case version of the data model's key that's passed in the object parameter of `MedusaService`. The name is pluralized for all operations except for the `retrieve` operation. +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. -Some examples of method names: +*** -- `createPosts` -- `createMyPosts` -- `retrievePost` -- `listPosts` +## Apply Or Condition -*** +```ts +const posts = await postModuleService.listPosts({ + $or: [ + { + name: "My Post", + }, + { + published_at: { + $lt: new Date(), + }, + }, + ], +}) +``` -## Methods Reference +To use an `or` condition, pass to the filter object the `$or` property, whose value is an array of filters. -The reference uses only the operation name to refer to the method. +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. # create Method - Service Factory Reference @@ -30933,795 +31078,652 @@ const posts = await postModuleService.createPosts([ If an array is passed of the method, an array of the created records is also returned. -# delete Method - Service Factory Reference - -This method deletes one or more records. - -## Delete One Record - -```ts -await postModuleService.deletePosts("123") -``` - -To delete one record, pass its ID as a parameter of the method. +# list Method - Service Factory Reference -*** +This method retrieves a list of records. -## Delete Multiple Records +## Retrieve List of Records ```ts -await postModuleService.deletePosts([ - "123", - "321", -]) +const posts = await postModuleService.listPosts() ``` -To delete multiple records, pass an array of IDs as a parameter of the method. +If no parameters are passed, the method returns an array of the first `15` records. *** -## Delete Records Matching Filters +## Filter Records ```ts -await postModuleService.deletePosts({ - name: "My Post", +const posts = await postModuleService.listPosts({ + id: ["123", "321"], }) ``` -To 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). - - -# 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 +To retrieve records matching a set of filters, pass an object of the filters as a first parameter. -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. +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). -For example, the returned object of the above example is: +### Returns -```ts -restoredPosts = { - post_id: ["123"], -} -``` +The method returns an array of the first `15` records matching the filters. *** -## Restore Multiple Records +## 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 restoredPosts = await postModuleService.restorePosts([ - "123", - "321", -]) +const posts = await postModuleService.listPosts({}, { + relations: ["author"], +}) ``` ### Parameters -To restore multiple records, pass an array of IDs as a parameter of the method. +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 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", - ], -} -``` +The method returns an array of the first `15` records matching the filters. *** -## Restore Records Matching Filters +## Select Properties ```ts -const restoredPosts = await postModuleService.restorePosts({ - name: "My Post", +const posts = await postModuleService.listPosts({}, { + select: ["id", "name"], }) ``` ### 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", - ], -} -``` - - -# retrieve Method - Service Factory Reference - -This method retrieves one record of the data model by its ID. - -## Retrieve a Record - -```ts -const post = await postModuleService.retrievePost("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. -Pass the ID of the record to retrieve. +`select`'s value is an array of property names to retrieve. ### Returns -The method returns the record as an object. +The method returns an array of the first `15` records matching the filters. *** -## Retrieve a Record's 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). +## Paginate Relations ```ts -const post = await postModuleService.retrievePost("123", { - relations: ["author"], +const posts = await postModuleService.listPosts({}, { + take: 20, + skip: 10, }) ``` ### Parameters -To retrieve the data model with relations, pass as a second parameter of the method an object with the property `relations`. `relations`'s value is an array of relation names. +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 the record as an object. +The method returns an array of records. The number of records is less than or equal to `take`'s value. *** -## Select Properties to Retrieve +## Sort Records ```ts -const post = await postModuleService.retrievePost("123", { - select: ["id", "name"], +const posts = await postModuleService.listPosts({}, { + order: { + name: "ASC", + }, }) ``` ### Parameters -By default, all of the record's properties are retrieved. To select specific ones, pass in the second object parameter a `select` property. Its value is an array of property names. +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 the record as an object. +The method returns an array of the first `15` records matching the filters. -# softDelete Method - Service Factory Reference +# delete Method - Service Factory Reference -This method soft deletes one or more records of the data model. +This method deletes one or more records. -## Soft Delete One Record +## Delete One Record ```ts -const deletedPosts = await postModuleService.softDeletePosts( - "123" -) +await postModuleService.deletePosts("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"], -} -``` +To delete one record, pass its ID as a parameter of the method. *** -## Soft Delete Multiple Records +## Delete Multiple Records ```ts -const deletedPosts = await postModuleService.softDeletePosts([ +await postModuleService.deletePosts([ "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", - ], -} -``` +To delete multiple records, pass an array of IDs as a parameter of the method. *** -## Soft Delete Records Matching Filters +## Delete Records Matching Filters ```ts -const deletedPosts = await postModuleService.softDeletePosts({ +await postModuleService.deletePosts({ name: "My Post", }) ``` -### Parameters - -To soft delete records matching a set of filters, pass an object of filters as a parameter. +To 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"], -} -``` - -# update Method - Service Factory Reference +# listAndCount Method - Service Factory Reference -This method updates one or more records of the data model. +This method retrieves a list of records with the total count. -## Update One Record +## Retrieve List of Records ```ts -const post = await postModuleService.updatePosts({ - id: "123", - name: "My Post", -}) +const [posts, count] = await postModuleService.listAndCountPosts() ``` -### Parameters - -To update one record, pass an object that at least has an `id` property, identifying the ID of the record to update. - -You can pass in the same object any other properties to update. - -### Returns +If no parameters are passed, the method returns an array with two items: -The method returns the updated record as an object. +1. The first is an array of the first `15` records retrieved. +2. The second is the total count of records. *** -## Update Multiple Records +## Filter Records ```ts -const posts = await postModuleService.updatePosts([ - { - id: "123", - name: "My Post", - }, - { - id: "321", - published_at: new Date(), - }, -]) +const [posts, count] = await postModuleService.listAndCountPosts({ + id: ["123", "321"], +}) ``` ### Parameters -To update multiple records, pass an array of objects. Each object has at least an `id` property, identifying the ID of the record to update. +To retrieve records matching a set of filters, pass an object of the filters as a first parameter. -You can pass in each object any other properties to update. +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 objects of updated records. +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. *** -## Update Records Matching a Filter +## 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 = await postModuleService.updatePosts({ - selector: { - name: "My Post", - }, - data: { - published_at: new Date(), - }, +const [posts, count] = await postModuleService.listAndCountPosts({}, { + relations: ["author"], }) ``` ### Parameters -To update records that match specified filters, pass as a parameter an object having two properties: - -- `selector`: An object of filters that a record must match to be updated. -- `data`: An object of the properties to update in every record that match the filters in `selector`. - -In the example above, you update the `published_at` property of every post record whose name is `My Post`. - -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). +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 of objects of updated records. +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. *** -## Multiple Record Updates with Filters +## Select Properties ```ts -const posts = await postModuleService.updatePosts([ - { - selector: { - name: "My Post", - }, - data: { - published_at: new Date(), - }, - }, - { - selector: { - name: "Another Post", - }, - data: { - metadata: { - external_id: "123", - }, - }, - }, -]) +const [posts, count] = await postModuleService.listAndCountPosts({}, { + select: ["id", "name"], +}) ``` ### Parameters -To update records matching different sets of filters, pass an array of objects, each having two properties: - -- `selector`: An object of filters that a record must match to be updated. -- `data`: An object of the properties to update in every record that match the filters in `selector`. - -In the example above, you update the `published_at` property of post records whose name is `My Post`, and update the `metadata` property of post records whose name is `Another Post`. +By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property. -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). +`select`'s value is an array of property names to retrieve. ### Returns -The method returns an array of objects of updated records. - - -# Filter Records - Service Factory Reference - -Many of the service factory's generated methods allow passing filters to perform an operation, such as to update or delete records matching the filters. - -This guide provides examples of using filters. +The method returns an array with two items: -The `list` method is used in the example snippets of this reference, but you can use the same filtering mechanism with any method that accepts filters. +1. The first is an array of the first `15` records retrieved. +2. The second is the total count of records. *** -## Match Exact Value +## Paginate Relations ```ts -const posts = await postModuleService.listPosts({ - name: "My Post 2", +const [posts, count] = await postModuleService.listAndCountPosts({}, { + take: 20, + skip: 10, }) ``` -If you pass a property with its value, only records whose properties exactly match the value are selected. - -In the example above, only posts having the name `My Post 2` are retrieved. +### Parameters -*** +To paginate the returned records, the second object parameter accepts the following properties: -## Match Multiple Values +- `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`. -```ts -const posts = await postModuleService.listPosts({ - views: [ - 50, - 100, - ], -}) -``` +### Returns -To find records with a property matching multiple values, pass an array of those values as the property's value in the filter. +The method returns an array with two items: -In the example above, only posts having either `50` or `100` views are retrieved. +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. *** -## Don't Match Values +## Sort Records ```ts -const posts = await postModuleService.listPosts({ - name: { - $nin: [ - "My Post", - ], +const [posts, count] = await postModuleService.listAndCountPosts({}, { + order: { + name: "ASC", }, }) ``` -To find records with a property that doesn't match one or more values, pass an object with a `$nin` property. Its value is an array of multiple values that a record's property shouldn't match. - -In the example above, only posts that don't have the name `My Post` are retrieved. +### 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: -## Match Text Like Value +- `ASC` to sort by this property in the ascending order. +- `DESC` to sort by this property in the descending order. -This filter only applies to text-like properties, including `id` and `enum` properties. +### Returns -```ts -const posts = await postModuleService.listPosts({ - name: { - $like: "My%", - }, -}) -``` +The method returns an array with two items: -To perform a `like` filter on a record's property, set the property's value to an object with a `$like` property. Its value is the string to use when applying the `like` filter. +1. The first is an array of the first `15` records retrieved. +2. The second is the total count of records. -The example above matches all posts whose name starts with `My`. -*** +# restore Method - Service Factory Reference -## Apply Range Filters +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). -This filter only applies to the `number` and `dateTime` properties. +## Restore One Record ```ts -const posts = await postModuleService.listPosts({ - published_at: { - $lt: new Date(), - }, -}) +const restoredPosts = await postModuleService.restorePosts("123") ``` -To filter a record's property to be within a range, set the property's value to an object with any of the following properties: - -1. `$lt`: The property's value must be less than the supplied value. -2. `$lte`: The property's value must be less than or equal to the supplied value. -3. `$gt`: The property's value must be greater than the supplied value. -4. `$gte`: The property's value must be greater than or equal the supplied value. +### Parameters -In the example above, only posts whose `published_at` property is before the current date and time are retrieved. +To restore one record, pass its ID as a parameter of the method. -### Example: Retrieve Posts Published Today +### Returns -```ts -const startToday = new Date() -startToday.setHours(0, 0, 0, 0) +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. -const endToday = new Date() -endToday.setHours(23, 59, 59, 59) +For example, the returned object of the above example is: -const posts = await postModuleService.listPosts({ - published_at: { - $gte: startToday, - $lte: endToday, - }, -}) +```ts +restoredPosts = { + post_id: ["123"], +} ``` -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 +## Restore Multiple Records ```ts -const posts = await postModuleService.listPosts({ - $or: [ - { - name: "My Post", - }, - { - published_at: { - $lt: new Date(), - }, - }, - ], -}) +const restoredPosts = await postModuleService.restorePosts([ + "123", + "321", +]) ``` -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. +### Parameters +To restore multiple records, pass an array of IDs as a parameter of the method. -# listAndCount Method - Service Factory Reference +### Returns -This method retrieves a list of records with the total count. +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. -## Retrieve List of Records +For example, the returned object of the above example is: ```ts -const [posts, count] = await postModuleService.listAndCountPosts() +restoredPosts = { + post_id: [ + "123", + "321", + ], +} ``` -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 +## Restore Records Matching Filters ```ts -const [posts, count] = await postModuleService.listAndCountPosts({ - id: ["123", "321"], +const restoredPosts = await postModuleService.restorePosts({ + name: "My Post", }) ``` ### Parameters -To retrieve records matching a set of filters, pass an object of the filters as a first parameter. +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 array with two items: +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. -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. +For example, the returned object of the above example is: -*** +```ts +restoredPosts = { + post_id: [ + "123", + ], +} +``` -## 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). +# softDelete Method - Service Factory Reference + +This method soft deletes one or more records of the data model. + +## Soft Delete One Record ```ts -const [posts, count] = await postModuleService.listAndCountPosts({}, { - relations: ["author"], -}) +const deletedPosts = await postModuleService.softDeletePosts( + "123" +) ``` ### 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 soft delete a record, pass its ID as a parameter of the method. ### Returns -The method returns an array with two items: +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. -1. The first is an array of the first `15` records retrieved. -2. The second is the total count of records. +For example, the returned object of the above example is: + +```ts +deletedPosts = { + post_id: ["123"], +} +``` *** -## Select Properties +## Soft Delete Multiple Records ```ts -const [posts, count] = await postModuleService.listAndCountPosts({}, { - select: ["id", "name"], -}) +const deletedPosts = await postModuleService.softDeletePosts([ + "123", + "321", +]) ``` ### 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 multiple records, pass an array of IDs as a parameter of the method. ### Returns -The method returns an array with two items: +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. -1. The first is an array of the first `15` records retrieved. -2. The second is the total count of records. +For example, the returned object of the above example is: + +```ts +deletedPosts = { + post_id: [ + "123", + "321", + ], +} +``` *** -## Paginate Relations +## Soft Delete Records Matching Filters ```ts -const [posts, count] = await postModuleService.listAndCountPosts({}, { - take: 20, - skip: 10, +const deletedPosts = await postModuleService.softDeletePosts({ + name: "My Post", }) ``` ### Parameters -To paginate the returned records, the second object parameter accepts the following properties: +To soft delete records matching a set of filters, pass an object of filters as a parameter. -- `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`. +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: +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. -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. +For example, the returned object of the above example is: -*** +```ts +deletedPosts = { + post_id: ["123"], +} +``` -## Sort Records + +# retrieve Method - Service Factory Reference + +This method retrieves one record of the data model by its ID. + +## Retrieve a Record ```ts -const [posts, count] = await postModuleService.listAndCountPosts({}, { - order: { - name: "ASC", - }, -}) +const post = await postModuleService.retrievePost("123") ``` ### 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. +Pass the ID of the record to retrieve. ### 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 the record as an object. -# list Method - Service Factory Reference +*** -This method retrieves a list of records. +## Retrieve a Record's Relations -## Retrieve List of 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 posts = await postModuleService.listPosts() +const post = await postModuleService.retrievePost("123", { + relations: ["author"], +}) ``` -If no parameters are passed, the method returns an array of the first `15` records. +### Parameters + +To retrieve the data model with relations, pass as a second parameter of the method an object with the property `relations`. `relations`'s value is an array of relation names. + +### Returns + +The method returns the record as an object. *** -## Filter Records +## Select Properties to Retrieve ```ts -const posts = await postModuleService.listPosts({ - id: ["123", "321"], +const post = await postModuleService.retrievePost("123", { + select: ["id", "name"], }) ``` ### 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). +By default, all of the record's properties are retrieved. To select specific ones, pass in the second object parameter a `select` property. Its value is an array of property names. ### Returns -The method returns an array of the first `15` records matching the filters. +The method returns the record as an object. -*** -## Retrieve Relations +# update Method - Service Factory Reference -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). +This method updates one or more records of the data model. + +## Update One Record ```ts -const posts = await postModuleService.listPosts({}, { - relations: ["author"], +const post = await postModuleService.updatePosts({ + id: "123", + 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. +To update one record, pass an object that at least has an `id` property, identifying the ID of the record to update. + +You can pass in the same object any other properties to update. ### Returns -The method returns an array of the first `15` records matching the filters. +The method returns the updated record as an object. *** -## Select Properties +## Update Multiple Records ```ts -const posts = await postModuleService.listPosts({}, { - select: ["id", "name"], -}) +const posts = await postModuleService.updatePosts([ + { + id: "123", + name: "My Post", + }, + { + id: "321", + published_at: new Date(), + }, +]) ``` ### Parameters -By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property. +To update multiple records, pass an array of objects. Each object has at least an `id` property, identifying the ID of the record to update. -`select`'s value is an array of property names to retrieve. +You can pass in each object any other properties to update. ### Returns -The method returns an array of the first `15` records matching the filters. +The method returns an array of objects of updated records. *** -## Paginate Relations +## Update Records Matching a Filter ```ts -const posts = await postModuleService.listPosts({}, { - take: 20, - skip: 10, +const posts = await postModuleService.updatePosts({ + selector: { + name: "My Post", + }, + data: { + published_at: new Date(), + }, }) ``` ### Parameters -To paginate the returned records, the second object parameter accepts the following properties: +To update records that match specified filters, pass as a parameter an object having two 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`. +- `selector`: An object of filters that a record must match to be updated. +- `data`: An object of the properties to update in every record that match the filters in `selector`. + +In the example above, you update the `published_at` property of every post record whose name is `My Post`. + +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 records. The number of records is less than or equal to `take`'s value. +The method returns an array of objects of updated records. *** -## Sort Records +## Multiple Record Updates with Filters ```ts -const posts = await postModuleService.listPosts({}, { - order: { - name: "ASC", +const posts = await postModuleService.updatePosts([ + { + selector: { + name: "My Post", + }, + data: { + published_at: new Date(), + }, }, -}) + { + selector: { + name: "Another Post", + }, + data: { + metadata: { + external_id: "123", + }, + }, + }, +]) ``` ### 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 update records matching different sets of filters, pass an array of objects, each having two properties: -- `ASC` to sort by this property in the ascending order. -- `DESC` to sort by this property in the descending order. +- `selector`: An object of filters that a record must match to be updated. +- `data`: An object of the properties to update in every record that match the filters in `selector`. + +In the example above, you update the `published_at` property of post records whose name is `My Post`, and update the `metadata` property of post records whose name is `Another Post`. + +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 array of objects of updated records. @@ -32165,125 +32167,6 @@ How to install and setup Medusa UI. -# Medusa Admin Extension - -How to install and use Medusa UI for building Admin extensions. - -## Installation - -*** - -The `@medusajs/ui` package is a already installed as a dependency of the `@medusajs/admin` package. Due to this you can simply import the package and use it in your local Admin extensions. - -If you are building a Admin extension as part of a Medusa plugin, you can install the package as a dependency of your plugin. - -```bash -npm install @medusajs/ui -``` - -## Configuration - -*** - -The configuration of the UI package is handled by the `@medusajs/admin` package. Therefore, you do not need to any additional configuration to use the UI package in your Admin extensions. - - -# Standalone Project - -How to install and use Medusa UI in a standalone project. - -## Installation - -*** - -Medusa UI is a React UI library and while it's intended for usage within Medusa projects, it can also be used in any React project. - -### Install Medusa UI - -Install the React UI library with the following command: - -```bash -npm install @medusajs/ui -``` - -### Configuring Tailwind CSS - -The components are styled using Tailwind CSS, and in order to use them, you will need to install Tailwind CSS in your project as well. -For more information on how to install Tailwind CSS, please refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs/installation). - -All of the classes used for Medusa UI are shipped as a Tailwind CSS customization. -You can install it with the following command: - -```bash -npm install @medusajs/ui-preset -``` - -After you have installed Tailwind CSS and the Medusa UI preset, you need to add the following to your `tailwind.config.js`file: - -```tsx -module.exports = { - presets: [require("@medusajs/ui-preset")], - // ... -} -``` - -In order for the styles to be applied correctly to the components, you will also need to ensure that -`@medusajs/ui` is included in the content field of your `tailwind.config.js` file: - -```tsx -module.exports = { - content: [ - // ... - "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}", - ], - // ... -} -``` - -If you are working within a monorepo, you may need to add the path to the `@medusajs/ui` package in your `tailwind.config.js` like so: - -```tsx -const path = require("path") - -const uiPath = path.resolve( - require.resolve("@medusajs/ui"), - "../..", - "\*_/_.{js,jsx,ts,tsx}" -) - -module.exports = { - content: [ - // ... - uiPath, - ], - // ... -} - -``` - -## Start building - -*** - -You are now ready to start building your application with Medusa UI. You can import the components like so: - -```tsx -import { Button, Drawer } from "@medusajs/ui" -``` - -## Updating UI Packages - -*** - -Medusa's design-system packages, including `@medusajs/ui`, `@medusajs/ui-preset`, and `@medusajs/ui-icons`, are versioned independently. However, they're still part of the latest Medusa release. So, you can browse the [release notes](https://github.com/medusajs/medusa/releases) to see if there are any breaking changes to these packages. - -To update these packages, update their version in your `package.json` file and re-install dependencies. For example: - -```bash -npm install @medusajs/ui -``` - - # Alert A component for displaying important messages. @@ -38679,6 +38562,125 @@ If you're using the `Tooltip` component in a project other than the Medusa Admin - disableHoverableContent: (boolean) When \`true\`, trying to hover the content will result in the tooltip closing as the pointer leaves the trigger. +# Medusa Admin Extension + +How to install and use Medusa UI for building Admin extensions. + +## Installation + +*** + +The `@medusajs/ui` package is a already installed as a dependency of the `@medusajs/admin` package. Due to this you can simply import the package and use it in your local Admin extensions. + +If you are building a Admin extension as part of a Medusa plugin, you can install the package as a dependency of your plugin. + +```bash +npm install @medusajs/ui +``` + +## Configuration + +*** + +The configuration of the UI package is handled by the `@medusajs/admin` package. Therefore, you do not need to any additional configuration to use the UI package in your Admin extensions. + + +# Standalone Project + +How to install and use Medusa UI in a standalone project. + +## Installation + +*** + +Medusa UI is a React UI library and while it's intended for usage within Medusa projects, it can also be used in any React project. + +### Install Medusa UI + +Install the React UI library with the following command: + +```bash +npm install @medusajs/ui +``` + +### Configuring Tailwind CSS + +The components are styled using Tailwind CSS, and in order to use them, you will need to install Tailwind CSS in your project as well. +For more information on how to install Tailwind CSS, please refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs/installation). + +All of the classes used for Medusa UI are shipped as a Tailwind CSS customization. +You can install it with the following command: + +```bash +npm install @medusajs/ui-preset +``` + +After you have installed Tailwind CSS and the Medusa UI preset, you need to add the following to your `tailwind.config.js`file: + +```tsx +module.exports = { + presets: [require("@medusajs/ui-preset")], + // ... +} +``` + +In order for the styles to be applied correctly to the components, you will also need to ensure that +`@medusajs/ui` is included in the content field of your `tailwind.config.js` file: + +```tsx +module.exports = { + content: [ + // ... + "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}", + ], + // ... +} +``` + +If you are working within a monorepo, you may need to add the path to the `@medusajs/ui` package in your `tailwind.config.js` like so: + +```tsx +const path = require("path") + +const uiPath = path.resolve( + require.resolve("@medusajs/ui"), + "../..", + "\*_/_.{js,jsx,ts,tsx}" +) + +module.exports = { + content: [ + // ... + uiPath, + ], + // ... +} + +``` + +## Start building + +*** + +You are now ready to start building your application with Medusa UI. You can import the components like so: + +```tsx +import { Button, Drawer } from "@medusajs/ui" +``` + +## Updating UI Packages + +*** + +Medusa's design-system packages, including `@medusajs/ui`, `@medusajs/ui-preset`, and `@medusajs/ui-icons`, are versioned independently. However, they're still part of the latest Medusa release. So, you can browse the [release notes](https://github.com/medusajs/medusa/releases) to see if there are any breaking changes to these packages. + +To update these packages, update their version in your `package.json` file and re-install dependencies. For example: + +```bash +npm install @medusajs/ui +``` + + # clx Utility function for working with classNames. From 0d58c7fee28bd74dcf65ea558a7edbbd64bebda8 Mon Sep 17 00:00:00 2001 From: Shahed Nasser <shahednasser@gmail.com> Date: Tue, 4 Mar 2025 12:17:01 +0200 Subject: [PATCH 3/4] fix lint error --- .../api-routes/middlewares/page.mdx | 4 +- www/apps/book/generated/edit-dates.mjs | 2 +- www/apps/book/public/llms-full.txt | 27398 ++++++++-------- 3 files changed, 13702 insertions(+), 13702 deletions(-) diff --git a/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx index 1d7f3083ce340..fc488afe14e53 100644 --- a/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx +++ b/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx @@ -295,12 +295,12 @@ export default defineMiddlewares({ }, { matcher: "/custom*", - method: ["GET"] + method: ["GET"], middlewares: [/* ... */], }, { matcher: "/custom/:id", - method: ["GET"] + method: ["GET"], middlewares: [/* ... */], }, ], diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index f7cd9eae862ca..baf4b59610006 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -53,7 +53,7 @@ export const generatedEditDates = { "app/learn/fundamentals/admin/tips/page.mdx": "2025-02-24T09:52:06.901Z", "app/learn/fundamentals/api-routes/cors/page.mdx": "2024-12-09T13:04:04.357Z", "app/learn/fundamentals/admin/ui-routes/page.mdx": "2025-02-24T09:35:11.752Z", - "app/learn/fundamentals/api-routes/middlewares/page.mdx": "2025-03-04T10:15:17.128Z", + "app/learn/fundamentals/api-routes/middlewares/page.mdx": "2025-03-04T10:16:15.029Z", "app/learn/fundamentals/modules/isolation/page.mdx": "2024-12-09T11:02:38.087Z", "app/learn/fundamentals/data-models/configure-properties/page.mdx": "2024-10-21T13:30:21.368Z", "app/learn/fundamentals/data-models/index/page.mdx": "2024-10-21T13:30:21.368Z", diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index dc084c7a8dd69..f802012bbe76f 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -131,73 +131,6 @@ The next chapter covers how you generally deploy the production build. You can also refer to the [deployment how-to guides](https://docs.medusajs.com/resources/deployment/index.html.md) for platform-specific how-to guides. -# Debugging and Testing - -In the next chapters, you’ll learn about the tools Medusa provides for testing and debugging your Medusa application. - -By the end of this chapter, you’ll learn: - -- How to use Medusa's `@medusajs/test-utils` test to write integration tests. -- How to use Medusa’s `Logger` utility to log messages. - - -# Medusa Deployment Overview - -In this chapter, you’ll learn the general approach to deploying the Medusa application. - -## Medusa Project Components - -A standard Medusa project is made up of: - -- Medusa application: The Medusa server and the Medusa Admin. -- One or more storefronts - - - -You deploy the Medusa application, with the server and admin, separately from the storefront. - -*** - -## Deploying the Medusa Application - -You must deploy the Medusa application before the storefront, as it connects to the server and won’t work without a deployed Medusa server URL. - -The Medusa application must be deployed to a hosting provider supporting Node.js server deployments, such as Railway, DigitalOcean, AWS, Heroku, etc… - - - -Your server connects to a PostgreSQL database, Redis, and other services relevant for your setup. Most hosting providers support deploying and managing these databases along with your Medusa server (such as Railway and DigitalOcean). - -When you deploy your Medusa application, you also deploy the Medusa Admin. For optimal experience, your hosting provider and plan must offer at least 2GB of RAM. - -### How to Deploy Medusa? - -Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance. - -With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: - -- Push to deploy. -- Multiple testing environments. -- Preview environments for new PRs. -- Test on production-like data. - -[Sign up and learn more about Medusa Cloud](https://medusajs.com/contact) - -To self-host Medusa, the [next chapter](https://docs.medusajs.com/learn/deployment/general/index.html.md) explains the general steps to deploy your Medusa application. Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for general and specific hosting providers. - -*** - -## Deploying the Storefront - -The storefront is deployed separately from the Medusa application, and the hosting options depend on the tools and frameworks you use to create the storefront. - -If you’re using the Next.js Starter storefront, you may deploy the storefront to any hosting provider that supports frontend frameworks, such as Vercel. - -Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the storefront on the free plan for development purposes, but for commercial projects, you must update your Vercel plan. - -Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for specific hosting providers. - - # Install Medusa In this chapter, you'll learn how to install and run a Medusa application. @@ -322,33 +255,78 @@ Refer to [this documentation](https://docs.medusajs.com/learn/update/index.html. In the next chapters, you'll learn about the architecture of your Medusa application, then learn how to customize your application to build custom features. -# More Resources +# Medusa Deployment Overview -The Development Resources documentation provides guides and references that are useful for your development. This documentation included links to parts of the Development Resources documentation where necessary. +In this chapter, you’ll learn the general approach to deploying the Medusa application. -Check out the Development Resources documentation [here](https://docs.medusajs.com/resources/index.html.md). +## Medusa Project Components +A standard Medusa project is made up of: -# Storefront Development +- Medusa application: The Medusa server and the Medusa Admin. +- One or more storefronts -The Medusa application is made up of a Node.js server and an admin dashboard. Storefronts are installed, built, and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience. + -You can build your storefront from scratch with your preferred tech stack, or start with our Next.js Starter storefront. The Next.js Starter storefront provides rich commerce features and a sleek design. Developers and businesses can use it as-is or build on top of it to tailor it for the business's unique use case, design, and customer experience. +You deploy the Medusa application, with the server and admin, separately from the storefront. -- [Install Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) -- [Build Custom Storefront](https://docs.medusajs.com/resources/storefront-development/index.html.md) +*** + +## Deploying the Medusa Application + +You must deploy the Medusa application before the storefront, as it connects to the server and won’t work without a deployed Medusa server URL. + +The Medusa application must be deployed to a hosting provider supporting Node.js server deployments, such as Railway, DigitalOcean, AWS, Heroku, etc… + + + +Your server connects to a PostgreSQL database, Redis, and other services relevant for your setup. Most hosting providers support deploying and managing these databases along with your Medusa server (such as Railway and DigitalOcean). + +When you deploy your Medusa application, you also deploy the Medusa Admin. For optimal experience, your hosting provider and plan must offer at least 2GB of RAM. + +### How to Deploy Medusa? + +Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance. + +With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: + +- Push to deploy. +- Multiple testing environments. +- Preview environments for new PRs. +- Test on production-like data. + +[Sign up and learn more about Medusa Cloud](https://medusajs.com/contact) + +To self-host Medusa, the [next chapter](https://docs.medusajs.com/learn/deployment/general/index.html.md) explains the general steps to deploy your Medusa application. Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for general and specific hosting providers. *** -## Passing a Publishable API Key in Storefront Requests +## Deploying the Storefront -When sending a request to an API route starting with `/store`, you must include a publishable API key in the header of your request. +The storefront is deployed separately from the Medusa application, and the hosting options depend on the tools and frameworks you use to create the storefront. -A publishable API key sets the scope of your request to one or more sales channels. +If you’re using the Next.js Starter storefront, you may deploy the storefront to any hosting provider that supports frontend frameworks, such as Vercel. -Then, when you retrieve products, only products of those sales channels are retrieved. This also ensures you retrieve correct inventory data, and associate created orders with the scoped sales channel. +Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the storefront on the free plan for development purposes, but for commercial projects, you must update your Vercel plan. -Learn more about passing the publishable API key in [this storefront development guide](https://docs.medusajs.com/resources/storefront-development/publishable-api-keys/index.html.md). +Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for specific hosting providers. + + +# Debugging and Testing + +In the next chapters, you’ll learn about the tools Medusa provides for testing and debugging your Medusa application. + +By the end of this chapter, you’ll learn: + +- How to use Medusa's `@medusajs/test-utils` test to write integration tests. +- How to use Medusa’s `Logger` utility to log messages. + + +# More Resources + +The Development Resources documentation provides guides and references that are useful for your development. This documentation included links to parts of the Development Resources documentation where necessary. + +Check out the Development Resources documentation [here](https://docs.medusajs.com/resources/index.html.md). # Updating Medusa @@ -457,6 +435,28 @@ npm install ``` +# Storefront Development + +The Medusa application is made up of a Node.js server and an admin dashboard. Storefronts are installed, built, and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience. + +You can build your storefront from scratch with your preferred tech stack, or start with our Next.js Starter storefront. The Next.js Starter storefront provides rich commerce features and a sleek design. Developers and businesses can use it as-is or build on top of it to tailor it for the business's unique use case, design, and customer experience. + +- [Install Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) +- [Build Custom Storefront](https://docs.medusajs.com/resources/storefront-development/index.html.md) + +*** + +## Passing a Publishable API Key in Storefront Requests + +When sending a request to an API route starting with `/store`, you must include a publishable API key in the header of your request. + +A publishable API key sets the scope of your request to one or more sales channels. + +Then, when you retrieve products, only products of those sales channels are retrieved. This also ensures you retrieve correct inventory data, and associate created orders with the scoped sales channel. + +Learn more about passing the publishable API key in [this storefront development guide](https://docs.medusajs.com/resources/storefront-development/publishable-api-keys/index.html.md). + + # Using TypeScript Aliases By default, Medusa doesn't support TypeScript aliases in production. @@ -501,530 +501,266 @@ import { BrandModuleService } from "@/modules/brand/service" ``` -# Configure Instrumentation +# Build Custom Features -In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. +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. -## Observability with OpenTelemtry +By following these guides, you'll add brands to the Medusa application that you can associate with products. -Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for: +To build a custom feature in Medusa, you need three main tools: -- HTTP requests -- Workflow executions -- Query usages -- Database queries and operations +- [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. -*** + -## How to Configure Instrumentation in Medusa? +*** -### Prerequisites +## Next Chapters: Brand Module Example -- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html) +The next chapters will guide you to: -### Install Dependencies +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. -Start by installing the following OpenTelemetry dependencies in your Medusa project: -```bash npm2yarn -npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg -``` +# Customizations Next Steps: Learn the Fundamentals -Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies: +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 @opentelemetry/exporter-zipkin -``` +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. -### Add instrumentation.ts +## Helpful Resources Guides -Next, create the file `instrumentation.ts` with the following content: +The [Development Resources](https://docs.medusajs.com/resources/index.html.md) documentation provides more helpful guides and references for your development journey. Some of these guides and references include: -```ts title="instrumentation.ts" -import { registerOtel } from "@medusajs/medusa" -import { ZipkinExporter } from "@opentelemetry/exporter-zipkin" +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. -// If using an exporter other than Zipkin, initialize it here. -const exporter = new ZipkinExporter({ - serviceName: "my-medusa-project", -}) +*** -export function register() { - registerOtel({ - serviceName: "medusajs", - // pass exporter - exporter, - instrument: { - http: true, - workflows: true, - query: true, - }, - }) -} -``` +## More Examples in Recipes -In the `instrumentation.ts` file, you export a `register` function that uses Medusa's `registerOtel` utility function. You also initialize an instance of the exporter, such as Zipkin, and pass it to the `registerOtel` function. +In the Development Resources documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more. -`registerOtel` accepts an object where you can pass any [NodeSDKConfiguration](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_node.NodeSDKConfiguration.html) property along with the following properties: +Refer to the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation to learn more. -The `NodeSDKConfiguration` properties are accepted since Medusa v2.5.1. -- serviceName: (\`string\`) The name of the service traced. -- exporter: (\[SpanExporter]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_sdk\_trace\_base.SpanExporter.html)) An instance of an exporter, such as Zipkin. -- instrument: (\`object\`) Options specifying what to trace. +# Customize Medusa Admin Dashboard - - http: (\`boolean\`) Whether to trace HTTP requests. +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). - - query: (\`boolean\`) Whether to trace Query usages. +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: - - workflows: (\`boolean\`) Whether to trace Workflow executions. +- 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). - - db: (\`boolean\`) Whether to trace database queries and operations. -- instrumentations: (\[Instrumentation\[]]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_instrumentation.Instrumentation.html)) Additional instrumentation options that OpenTelemetry accepts. +From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard *** -## Test it Out +## Next Chapters: View Brands in Dashboard -To test it out, start your exporter, such as Zipkin. +In the next chapters, you'll continue with the brands example to: -Then, start your Medusa application: +- 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. -```bash npm2yarn -npm run dev -``` -Try to open the Medusa Admin or send a request to an API route. +# Extend Core Commerce Features -If you check traces in your exporter, you'll find new traces reported. +In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features. -### Trace Span Names +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. -Trace span names start with the following keywords based on what it's reporting: +Medusa's framework and orchestration tools mitigate these issues while supporting all your customization needs: -- `{methodName} {URL}` when reporting HTTP requests, where `{methodName}` is the HTTP method, and `{URL}` is the URL the request is sent to. -- `route:` when reporting route handlers running on an HTTP request. -- `middleware:` when reporting a middleware running on an HTTP request. -- `workflow:` when reporting a workflow execution. -- `step:` when reporting a step in a workflow execution. -- `query.graph:` when reporting Query usages. -- `pg.query:` when reporting database queries and operations. +- [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. +*** -# Logging +## Next Chapters: Link Brands to Products Example -In this chapter, you’ll learn how to use Medusa’s logging utility. +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: -## Logger Class +- 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. -Medusa provides a `Logger` class with advanced logging functionalities. This includes configuring logging levels or saving logs to a file. -The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`. +# 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. -## How to Log a Message +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. -Resolve the `logger` using the Medusa container to log a message in your resource. +In Medusa, you integrate a third-party system by: -For example, create the file `src/jobs/log-message.ts` with the following content: +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. -```ts title="src/jobs/log-message.ts" highlights={highlights} -import { MedusaContainer } from "@medusajs/framework/types" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +*** -export default async function myCustomJob( - container: MedusaContainer -) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) +## Next Chapters: Sync Brands Example - logger.info("I'm using the logger!") -} +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: -export const config = { - name: "test-logger", - // execute every minute - schedule: "* * * * *", -} -``` +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. -This creates a scheduled job that resolves the `logger` from the Medusa container and uses it to log a message. -### Test the Scheduled Job +# Re-Use Customizations with Plugins -To test out the above scheduled job, start the Medusa application: +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. -```bash npm2yarn -npm run dev -``` +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. -After a minute, you'll see the following message as part of the logged messages: +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. -```text -info: I'm using the logger! -``` + -*** +Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM. -## Log Levels +To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -The `Logger` class has the following methods: -- `info`: The message is logged with level `info`. -- `warn`: The message is logged with level `warn`. -- `error`: The message is logged with level `error`. -- `debug`: The message is logged with level `debug`. +# Medusa's Architecture -Each of these methods accepts a string parameter to log in the terminal with the associated level. +In this chapter, you'll learn about the architectural layers in Medusa. -*** +## HTTP, Workflow, and Module Layers -## Logging Configurations +Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes. -### Log Level +In a common Medusa application, requests go through four layers in the stack. In order of entry, those are: -The available log levels, from lowest to highest levels, are: +1. API Routes (HTTP): Our API Routes are the typical entry point. +2. Workflows: API Routes consume workflows that hold the opinionated business logic of your application. +3. Modules: Workflows use domain-specific modules for resource management. +4. Data store: Modules query the underlying datastore, which is a PostgreSQL database in common cases. -1. `silly` (default, meaning messages of all levels are logged) -2. `debug` -3. `info` -4. `warn` -5. `error` +These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -You can change that by setting the `LOG_LEVEL` environment variable to the minimum level you want to be logged. + -For example: +*** -```bash -LOG_LEVEL=error -``` +## Database Layer -This logs `error` messages only. +The Medusa application injects into each module a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. -The environment variable must be set as a system environment variable and not in `.env`. +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -### Save Logs in a File + -Aside from showing the logs in the terminal, you can save the logs in a file by setting the `LOG_FILE` environment variable to the path of the file relative to the Medusa server’s root directory. +*** -For example: +## Service Integrations -```bash -LOG_FILE=all.log -``` +Third-party services are integrated through commerce and architectural modules. You also create custom third-party integrations through a custom module. -Your logs are now saved in the `all.log` file at the root of your Medusa application. +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -The environment variable must be set as a system environment variable and not in `.env`. +### Commerce Modules -*** +[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 integrate Stripe through a payment module provider. -## Show Log with Progress + -The `Logger` class has an `activity` method used to log a message of level `info`. If the Medusa application is running in a development environment, a spinner starts to show the activity's progress. +### Architectural Modules -For example: +[Architectural modules](https://docs.medusajs.com/resources/architectural-modules/index.html.md) integrate third-party services and systems for architectural features. For example, you integrate Redis as a pub/sub service to send events, or SendGrid to send notifications. -```ts title="src/jobs/log-message.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + -export default async function myCustomJob( - container: MedusaContainer -) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) +*** - const activityId = logger.activity("First log message") +## Full Diagram of Medusa's Architecture - logger.progress(activityId, `Second log message`) +The following diagram illustrates Medusa's architecture over the three layers. - logger.success(activityId, "Last log message") -} -``` + -The `activity` method returns the ID of the started activity. This ID can then be passed to one of the following methods of the `Logger` class: -- `progress`: Log a message of level `info` that indicates progress within that same activity. -- `success`: Log a message of level `info` that indicates that the activity has succeeded. This also ends the associated activity. -- `failure`: Log a message of level `error` that indicates that the activity has failed. This also ends the associated activity. +# General Medusa Application Deployment Guide -If you configured the `LOG_LEVEL` environment variable to a level higher than those associated with the above methods, their messages won’t be logged. +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. +Find how-to guides for specific platforms in [this documentation](https://docs.medusajs.com/resources/deployment/index.html.md). -# Medusa Testing Tools +Want Medusa to manage and maintain your infrastructure? [Sign up and learn more about Medusa Cloud](https://medusajs.com/contact) -In this chapter, you'll learn about Medusa's testing tools and how to install and configure them. +Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance. -## @medusajs/test-utils Package +With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: -Medusa provides a Testing Framework to create integration tests for your custom API routes, modules, or other Medusa customizations. +- Push to deploy. +- Multiple testing environments. +- Preview environments for new PRs. +- Test on production-like data. -To use the Testing Framework, install `@medusajs/test-utils` as a `devDependency`: +### Prerequisites -```bash npm2yarn -npm install --save-dev @medusajs/test-utils@latest -``` +- [Medusa application’s codebase hosted on GitHub repository.](https://docs.medusajs.com/learn/index.html.md) + +## Hosting Provider Requirements + +When you deploy your Medusa application, make sure your chosen hosting provider supports deploying the following resources: + +1. PostgreSQL database: If your hosting provider doesn't support database hosting, you must find another hosting provider for the PostgreSQL database. +2. Redis database: If your hosting provider doesn't support database hosting, you must find another hosting provider for the Redis database. +3. Medusa application in server and worker mode. This means your hosting provider should support deploying two applications or instances from the same codebase. +4. For optimal experience, the hosting provider and plan must offer at least 2GB of RAM. *** -## Install and Configure Jest +## 1. Configure Medusa Application -Writing tests with `@medusajs/test-utils`'s tools requires installing and configuring Jest in your project. +### Worker Mode -Run the following command to install the required Jest dependencies: +The `workerMode` configuration determines which mode the Medusa application runs in. -```bash npm2yarn -npm install --save-dev jest @types/jest @swc/jest -``` +When you deploy the Medusa application, you deploy two instances: one in server mode, and one in worker mode. -Then, create the file `jest.config.js` with the following content: +Learn more about the `workerMode` configuration in [this document](https://docs.medusajs.com/resources/references/medusa-config#workermode/index.html.md). -```js title="jest.config.js" -const { loadEnv } = require("@medusajs/framework/utils") -loadEnv("test", process.cwd()) +So, add the following configuration in `medusa-config.ts`: -module.exports = { - transform: { - "^.+\\.[jt]s$": [ - "@swc/jest", - { - jsc: { - parser: { syntax: "typescript", decorators: true }, - }, - }, - ], +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + // ... + workerMode: process.env.MEDUSA_WORKER_MODE as "shared" | "worker" | "server", }, - testEnvironment: "node", - moduleFileExtensions: ["js", "ts", "json"], - modulePathIgnorePatterns: ["dist/"], - setupFiles: ["./integration-tests/setup.js"], -} - -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"] -} +}) ``` -Next, create the `integration-tests/setup.js` file with the following content: +Later, you’ll set different values of the `MEDUSA_WORKER_MODE` environment variable for each Medusa application deployment or instance. -```js title="integration-tests/setup.js" -const { MetadataStorage } = require("@mikro-orm/core") +### Configure Medusa Admin -MetadataStorage.clear() +You need to disable the Medusa Admin in the worker Medusa application, while keeping it enabled in the server Medusa application. So, add the following configuration in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + admin: { + disable: process.env.DISABLE_MEDUSA_ADMIN === "true", + }, +}) ``` -*** +Later, you’ll set different values of the `DISABLE_MEDUSA_ADMIN` environment variable. -## Add Test Commands - -Finally, add the following scripts to `package.json`: - -```json title="package.json" -"scripts": { - // ... - "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit", - "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit", - "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" -}, -``` - -You now have two commands: - -- `test:integration:http` to run integration tests (for example, for API routes and workflows) available under the `integration-tests/http` directory. -- `test:integration:modules` to run integration tests for modules available in any `__tests__` directory under `src/modules`. -- `test:unit` to run unit tests in any `__tests__` directory under the `src` directory. - -Medusa's Testing Framework works for integration tests only. You can write unit tests using Jest. - -*** - -## Test Tools and Writing Tests - -The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests. - - -# Build Custom Features - -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. - -To build a custom feature in Medusa, you need three main tools: - -- [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. - - - -*** - -## 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. - - -# 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. - - -# 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. - - -# 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. - - - -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). - - -# 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. - -Find how-to guides for specific platforms in [this documentation](https://docs.medusajs.com/resources/deployment/index.html.md). - -Want Medusa to manage and maintain your infrastructure? [Sign up and learn more about Medusa Cloud](https://medusajs.com/contact) - -Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance. - -With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: - -- Push to deploy. -- Multiple testing environments. -- Preview environments for new PRs. -- Test on production-like data. - -### Prerequisites - -- [Medusa application’s codebase hosted on GitHub repository.](https://docs.medusajs.com/learn/index.html.md) - -## Hosting Provider Requirements - -When you deploy your Medusa application, make sure your chosen hosting provider supports deploying the following resources: - -1. PostgreSQL database: If your hosting provider doesn't support database hosting, you must find another hosting provider for the PostgreSQL database. -2. Redis database: If your hosting provider doesn't support database hosting, you must find another hosting provider for the Redis database. -3. Medusa application in server and worker mode. This means your hosting provider should support deploying two applications or instances from the same codebase. -4. For optimal experience, the hosting provider and plan must offer at least 2GB of RAM. - -*** - -## 1. Configure Medusa Application - -### Worker Mode - -The `workerMode` configuration determines which mode the Medusa application runs in. - -When you deploy the Medusa application, you deploy two instances: one in server mode, and one in worker mode. - -Learn more about the `workerMode` configuration in [this document](https://docs.medusajs.com/resources/references/medusa-config#workermode/index.html.md). - -So, add the following configuration in `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - // ... - workerMode: process.env.MEDUSA_WORKER_MODE as "shared" | "worker" | "server", - }, -}) -``` - -Later, you’ll set different values of the `MEDUSA_WORKER_MODE` environment variable for each Medusa application deployment or instance. - -### Configure Medusa Admin - -You need to disable the Medusa Admin in the worker Medusa application, while keeping it enabled in the server Medusa application. So, add the following configuration in `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - admin: { - disable: process.env.DISABLE_MEDUSA_ADMIN === "true", - }, -}) -``` - -Later, you’ll set different values of the `DISABLE_MEDUSA_ADMIN` environment variable. - -### Configure Redis URL +### Configure Redis URL The `redisUrl` configuration specifies the connection URL to Redis to store the Medusa server's session. @@ -1255,154 +991,6 @@ 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. -# 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. - -## Helpful Resources Guides - -The [Development Resources](https://docs.medusajs.com/resources/index.html.md) documentation provides more helpful guides and references for your development journey. Some of these guides and references include: - -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 Development Resources documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more. - -Refer to the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation to learn more. - - -# Medusa's Architecture - -In this chapter, you'll learn about the architectural layers in Medusa. - -## HTTP, Workflow, and Module Layers - -Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes. - -In a common Medusa application, requests go through four layers in the stack. In order of entry, those are: - -1. API Routes (HTTP): Our API Routes are the typical entry point. -2. Workflows: API Routes consume workflows that hold the opinionated business logic of your application. -3. Modules: Workflows use domain-specific modules for resource management. -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). - - - -*** - -## Database Layer - -The Medusa application injects into each module a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. - -Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - - - -*** - -## Service Integrations - -Third-party services are integrated through commerce and architectural modules. You also create custom third-party integrations through a custom module. - -Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - -### Commerce Modules - -[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 integrate Stripe through a payment module provider. - - - -### Architectural Modules - -[Architectural modules](https://docs.medusajs.com/resources/architectural-modules/index.html.md) integrate third-party services and systems for architectural features. For example, you integrate Redis as a pub/sub service to send events, or SendGrid to send notifications. - - - -*** - -## Full Diagram of Medusa's Architecture - -The following diagram illustrates Medusa's architecture over the three layers. - - - - -# Next.js Starter Storefront - -The Medusa application is made up of a Node.js server and an admin dashboard. The storefront is installed and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience. - -The Next.js Starter storefront provides rich commerce features and a sleek design. Developers and businesses can use it as-is or build on top of it to tailor it for the business's unique use case, design, and customer experience. - -In this chapter, you’ll learn how to install the Next.js Starter storefront separately from the Medusa application. You can also install it while installing the Medusa application as explained in [the installation chapter](https://docs.medusajs.com/learn/installation/index.html.md). - -## Install Next.js Starter - -### Prerequisites - -- [Node.js v20+](https://nodejs.org/en/download) -- [Git CLI tool](https://git-scm.com/downloads) - -If you already have a Medusa application installed with at least one region, you can install the Next.js Starter storefront with the following steps: - -1. Clone the [Next.js Starter](https://github.com/medusajs/nextjs-starter-medusa): - -```bash -git clone https://github.com/medusajs/nextjs-starter-medusa my-medusa-storefront -``` - -2. Change to the `my-medusa-storefront` directory, install the dependencies, and rename the template environment variable file: - -```bash npm2yarn -cd my-medusa-storefront -npm install -mv .env.template .env.local -``` - -3. Set the Medusa application's publishable API key in the `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY` environment variable. You can retrieve the publishable API key in on the Medusa Admin dashboard by going to Settings -> Publishable API Keys - -```bash -NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_123... -``` - -4. While the Medusa application is running, start the Next.js Starter storefront: - -```bash npm2yarn -npm run dev -``` - -Your Next.js Starter storefront is now running at `http://localhost:8000`. - -*** - -## Customize Storefront - -To customize the storefront, refer to the following directories: - -- `src/app`: The storefront’s pages. -- `src/modules`: The storefront’s components. -- `src/styles`: The storefront’s styles. - -You can learn more about development with Next.js through [their documentation](https://nextjs.org/docs/getting-started). - -*** - -## Configurations and Integrations - -The Next.js Starter is compatible with some Medusa integrations out-of-the-box, such as the Stripe provider module. You can also change some of its configurations if necessary. - -Refer to the [Next.js Starter reference](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) for more details. - - # Admin Development In the next chapters, you'll learn more about possible admin customizations. @@ -1486,76 +1074,6 @@ curl http://localhost:9000/hello-world You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application. -# Custom CLI Scripts - -In this chapter, you'll learn how to create and execute custom scripts from Medusa's CLI tool. - -## What is a Custom CLI Script? - -A custom CLI script is a function to execute through Medusa's CLI tool. This is useful when creating custom Medusa tooling to run through the CLI. - -*** - -## How to Create a Custom CLI Script? - -To create a custom CLI script, create a TypeScript or JavaScript file under the `src/scripts` directory. The file must default export a function. - -For example, create the file `src/scripts/my-script.ts` with the following content: - -```ts title="src/scripts/my-script.ts" -import { - ExecArgs, - IProductModuleService, -} from "@medusajs/framework/types" -import { Modules } from "@medusajs/framework/utils" - -export default async function myScript({ container }: ExecArgs) { - const productModuleService: IProductModuleService = container.resolve( - Modules.PRODUCT - ) - - const [, count] = await productModuleService - .listAndCountProducts() - - console.log(`You have ${count} product(s)`) -} -``` - -The function receives as a parameter an object having a `container` property, which is an instance of the Medusa Container. Use it to resolve resources in your Medusa application. - -*** - -## How to Run Custom CLI Script? - -To run the custom CLI script, run the Medusa CLI's `exec` command: - -```bash -npx medusa exec ./src/scripts/my-script.ts -``` - -*** - -## Custom CLI Script Arguments - -Your script can accept arguments from the command line. Arguments are passed to the function's object parameter in the `args` property. - -For example: - -```ts -import { ExecArgs } from "@medusajs/framework/types" - -export default async function myScript({ args }: ExecArgs) { - console.log(`The arguments you passed: ${args}`) -} -``` - -Then, pass the arguments in the `exec` command after the file path: - -```bash -npx medusa exec ./src/scripts/my-script.ts arg1 arg2 -``` - - # Environment Variables In this chapter, you'll learn how environment variables are loaded in Medusa. @@ -1635,313 +1153,326 @@ You should opt for setting configurations in `medusa-config.ts` where possible. ||Whether to disable analytics data collection. Learn more in || -# Medusa Container - -In this chapter, you’ll learn about the Medusa container and how to use it. - -## What is the Medusa Container? - -The Medusa container is a registry of framework and commerce tools that's accessible across your application. Medusa automatically registers these tools in the container, including custom ones that you've built, so that you can use them in your customizations. - -In other platforms, if you have a resource A (for example, a class) that depends on a resource B, you have to manually add resource B to the container or specify it beforehand as A's dependency, which is often done in a file separate from A's code. This becomes difficult to manage as you maintain larger applications with many changing dependencies. - -Medusa simplifies this process by giving you access to the container, with the tools or resources already registered, at all times in your customizations. When you reach a point in your code where you need a tool, you resolve it from the container and use it. +# Events and Subscribers -For example, consider you're creating an API route that retrieves products based on filters using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md), a tool that fetches data across the application. In the API route's function, you can resolve Query from the container passed to the API route and use it: +In this chapter, you’ll learn about Medusa's event system, and how to handle events with subscribers. -```ts highlights={highlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +## Handle Core Commerce Flows with Events -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const query = req.scope.resolve("query") +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. - const { data: products } = await query.graph({ - entity: "product", - fields: ["*"], - filters: { - id: "prod_123", - }, - }) +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. - res.json({ - products, - }) -} -``` +You listen to an event in a subscriber, which is an asynchronous function that's executed when its associated event is emitted. -The API route accepts as a first parameter a request object that has a `scope` property, which is the Medusa container. It has a `resolve` method that resolves a resource from the container by the key it's registered with. + -You can learn more about how Query works in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). +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 Resources Registered in the Medusa Container +### List of Emitted Events -Find a full list of the registered resources and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) +Find a list of all emitted events in [this reference](https://docs.medusajs.com/resources/events-reference/index.html.md). *** -## How to Resolve From the Medusa Container +## How to Create a Subscriber? -This section gives quick examples of how to resolve resources from the Medusa container in customizations other than an API route, which is covered in the section above. +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. -### Subscriber +For example, create the file `src/subscribers/order-placed.ts` with the following content: -A [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) function, which is executed when an event is emitted, accepts as a parameter an object with a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: + -```ts highlights={subscriberHighlights} +```ts title="src/subscribers/product-created.ts" import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation" -export default async function productCreateHandler({ +export default async function orderPlacedHandler({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const query = container.resolve(ContainerRegistrationKeys.QUERY) + const logger = container.resolve("logger") - const { data: products } = await query.graph({ - entity: "product", - fields: ["*"], - filters: { - id: data.id, - }, - }) + logger.info("Sending confirmation email...") - console.log(`You created a product with the title ${products[0].title}`) + await sendOrderConfirmationWorkflow(container) + .run({ + input: { + id: data.id, + }, + }) } export const config: SubscriberConfig = { - event: `product.created`, + event: `order.placed`, } ``` -### Scheduled Job +This subscriber file exports: -A [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) function, which is executed at a specified interval, accepts the Medusa container as a parameter. Use its `resolve` method to resolve a resource by its registration key: +- 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. -```ts highlights={scheduledJobHighlights} -import { MedusaContainer } from "@medusajs/framework/types" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +The subscriber function receives an object as a parameter that has the following properties: -export default async function myCustomJob( - container: MedusaContainer -) { - const query = container.resolve(ContainerRegistrationKeys.QUERY) +- `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. - const { data: products } = await query.graph({ - entity: "product", - fields: ["*"], - filters: { - id: "prod_123", - }, - }) +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. - console.log( - `You have ${products.length} matching your filters.` - ) -} +*** -export const config = { - name: "every-minute-message", - // execute every minute - schedule: "* * * * *", -} +## Test the Subscriber + +To test the subscriber, start the Medusa application: + +```bash npm2yarn +npm run dev ``` -### Workflow Step +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: -A [step in a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function where you build durable execution logic across multiple modules, accepts in its second parameter a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: +```bash +info: Processing order.placed which has 1 subscribers +Sending confirmation email... +``` -```ts highlights={workflowStepsHighlight} -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +The first message indicates that the `order.placed` event was emitted, and the second one is the message logged from the subscriber. -const step1 = createStep("step-1", async (_, { container }) => { - const query = container.resolve(ContainerRegistrationKeys.QUERY) +*** - const { data: products } = await query.graph({ - entity: "product", - fields: ["*"], - filters: { - id: "prod_123", - }, - }) +## Event Module - return new StepResponse(products) -}) -``` +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. -### Module Services and Loaders +Medusa provides two Event Modules out of the box: -A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of functionalities for a single feature or domain, has its own container, so it can't resolve resources from the Medusa container. +- [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. -Learn more about the module's container in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md). +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). -# Events and Subscribers +# Custom CLI Scripts -In this chapter, you’ll learn about Medusa's event system, and how to handle events with subscribers. +In this chapter, you'll learn how to create and execute custom scripts from Medusa's CLI tool. -## Handle Core Commerce Flows with Events +## What is a Custom CLI Script? -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. +A custom CLI script is a function to execute through Medusa's CLI tool. This is useful when creating custom Medusa tooling to run through the CLI. -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. +## How to Create a Custom CLI Script? - +To create a custom CLI script, create a TypeScript or JavaScript file under the `src/scripts` directory. The file must default export a function. -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. +For example, create the file `src/scripts/my-script.ts` with the following content: -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. +```ts title="src/scripts/my-script.ts" +import { + ExecArgs, + IProductModuleService, +} from "@medusajs/framework/types" +import { Modules } from "@medusajs/framework/utils" -### List of Emitted Events +export default async function myScript({ container }: ExecArgs) { + const productModuleService: IProductModuleService = container.resolve( + Modules.PRODUCT + ) -Find a list of all emitted events in [this reference](https://docs.medusajs.com/resources/events-reference/index.html.md). + const [, count] = await productModuleService + .listAndCountProducts() + + console.log(`You have ${count} product(s)`) +} +``` + +The function receives as a parameter an object having a `container` property, which is an instance of the Medusa Container. Use it to resolve resources in your Medusa application. *** -## How to Create a Subscriber? +## How to Run Custom CLI Script? -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. +To run the custom CLI script, run the Medusa CLI's `exec` command: -For example, create the file `src/subscribers/order-placed.ts` with the following content: +```bash +npx medusa exec ./src/scripts/my-script.ts +``` - +*** -```ts title="src/subscribers/product-created.ts" -import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" -import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation" +## Custom CLI Script Arguments -export default async function orderPlacedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const logger = container.resolve("logger") +Your script can accept arguments from the command line. Arguments are passed to the function's object parameter in the `args` property. - logger.info("Sending confirmation email...") +For example: - await sendOrderConfirmationWorkflow(container) - .run({ - input: { - id: data.id, - }, - }) -} +```ts +import { ExecArgs } from "@medusajs/framework/types" -export const config: SubscriberConfig = { - event: `order.placed`, +export default async function myScript({ args }: ExecArgs) { + console.log(`The arguments you passed: ${args}`) } ``` -This subscriber file exports: +Then, pass the arguments in the `exec` command after the file path: -- 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. +```bash +npx medusa exec ./src/scripts/my-script.ts arg1 arg2 +``` -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. +# Medusa Container -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. +In this chapter, you’ll learn about the Medusa container and how to use it. -*** +## What is the Medusa Container? -## Test the Subscriber +The Medusa container is a registry of framework and commerce tools that's accessible across your application. Medusa automatically registers these tools in the container, including custom ones that you've built, so that you can use them in your customizations. -To test the subscriber, start the Medusa application: +In other platforms, if you have a resource A (for example, a class) that depends on a resource B, you have to manually add resource B to the container or specify it beforehand as A's dependency, which is often done in a file separate from A's code. This becomes difficult to manage as you maintain larger applications with many changing dependencies. -```bash npm2yarn -npm run dev -``` +Medusa simplifies this process by giving you access to the container, with the tools or resources already registered, at all times in your customizations. When you reach a point in your code where you need a tool, you resolve it from the container and use it. -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: +For example, consider you're creating an API route that retrieves products based on filters using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md), a tool that fetches data across the application. In the API route's function, you can resolve Query from the container passed to the API route and use it: -```bash -info: Processing order.placed which has 1 subscribers -Sending confirmation email... -``` +```ts highlights={highlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -The first message indicates that the `order.placed` event was emitted, and the second one is the message logged from the subscriber. +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const query = req.scope.resolve("query") -*** + const { data: products } = await query.graph({ + entity: "product", + fields: ["*"], + filters: { + id: "prod_123", + }, + }) -## Event Module + res.json({ + products, + }) +} +``` -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. +The API route accepts as a first parameter a request object that has a `scope` property, which is the Medusa container. It has a `resolve` method that resolves a resource from the container by the key it's registered with. -Medusa provides two Event Modules out of the box: +You can learn more about how Query works in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). -- [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). +## List of Resources Registered in the Medusa Container +Find a full list of the registered resources and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/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). +## How to Resolve From the Medusa Container -In the next chapters, you'll learn about defining data models in more details. You'll learn about: +This section gives quick examples of how to resolve resources from the Medusa container in customizations other than an API route, which is covered in the section above. -- 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. +### Subscriber +A [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) function, which is executed when an event is emitted, accepts as a parameter an object with a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: -# Plugins +```ts highlights={subscriberHighlights} +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -In this chapter, you'll learn what a plugin is in Medusa. +export default async function productCreateHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const query = container.resolve(ContainerRegistrationKeys.QUERY) -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + const { data: products } = await query.graph({ + entity: "product", + fields: ["*"], + filters: { + id: data.id, + }, + }) -## What is a Plugin? + console.log(`You created a product with the title ${products[0].title}`) +} -A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). +export const config: SubscriberConfig = { + event: `product.created`, +} +``` -Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project. +### Scheduled Job - +A [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) function, which is executed at a specified interval, accepts the Medusa container as a parameter. Use its `resolve` method to resolve a resource by its registration key: -Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md). +```ts highlights={scheduledJobHighlights} +import { MedusaContainer } from "@medusajs/framework/types" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -*** +export default async function myCustomJob( + container: MedusaContainer +) { + const query = container.resolve(ContainerRegistrationKeys.QUERY) -## Plugin vs Module + const { data: products } = await query.graph({ + entity: "product", + fields: ["*"], + filters: { + id: "prod_123", + }, + }) -A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase. + console.log( + `You have ${products.length} matching your filters.` + ) +} -A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it. +export const config = { + name: "every-minute-message", + // execute every minute + schedule: "* * * * *", +} +``` -For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service. +### Workflow Step -- You want to reuse your Medusa customizations across multiple projects. -- You want to share your Medusa customizations with the community. +A [step in a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function where you build durable execution logic across multiple modules, accepts in its second parameter a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: -- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow. +```ts highlights={workflowStepsHighlight} +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -*** +const step1 = createStep("step-1", async (_, { container }) => { + const query = container.resolve(ContainerRegistrationKeys.QUERY) -## How to Create a Plugin? + const { data: products } = await query.graph({ + entity: "product", + fields: ["*"], + filters: { + id: "prod_123", + }, + }) -The next chapter explains how you can create and publish a plugin. + return new StepResponse(products) +}) +``` -*** +### Module Services and Loaders -## Plugin Guides and Resources +A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of functionalities for a single feature or domain, has its own container, so it can't resolve resources from the Medusa container. -For more resources and guides related to plugins, refer to the [Resources documentation](https://docs.medusajs.com/resources/plugins/index.html.md). +Learn more about the module's container in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md). # Module Link @@ -2068,614 +1599,170 @@ export default defineLink( In this example, when a product is deleted, its linked `myCustom` record is also deleted. -# Modules +# Data Models Advanced Guides -In this chapter, you’ll learn about modules and how to create them. +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). -## What is a Module? +In the next chapters, you'll learn about defining data models in more details. You'll learn about: -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. +- 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. -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 implications on the existing setup. You can also re-use your modules across Medusa projects. +# Scheduled Jobs -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. +In this chapter, you’ll learn about scheduled jobs and how to use them. -- You want to build a custom feature related to a single domain or integrate a third-party service. +## What is a Scheduled Job? -- 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). +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. -*** +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. -## How to Create a Module? +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. -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. +- 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. -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 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`. +## How to Create a Scheduled Job? -### 1. Create Data Model +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. -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. +For example, create the file `src/jobs/hello-world.ts` with the following content: -You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a `Post` data model in the Blog Module, create the file `src/modules/blog/models/post.ts` with the following content: + - +```ts title="src/jobs/hello-world.ts" highlights={highlights} +import { MedusaContainer } from "@medusajs/framework/types" -```ts title="src/modules/blog/models/post.ts" -import { model } from "@medusajs/framework/utils" +export default async function greetingJob(container: MedusaContainer) { + const logger = container.resolve("logger") -const Post = model.define("post", { - id: model.id().primaryKey(), - title: model.text(), -}) + logger.info("Greeting!") +} -export default Post +export const config = { + name: "greeting-every-minute", + schedule: "* * * * *", +} ``` -You define the data model using the `define` method of the DML. It accepts two parameters: - -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. The schema's properties are defined using the `model`'s methods, such as `text` and `id`. - - Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`, so you don't need to add them manually. - -Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/property-types/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. -The code snippet above defines a `Post` data model with `id` and `title` properties. +You also export a `config` object that has the following properties: -### 2. Create Service +- `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. -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. Medusa registers the service in its [container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), allowing you to resolve and use it when building custom commerce flows. +This scheduled job executes every minute and logs into the terminal `Greeting!`. -You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, to create the Blog Module's service, create the file `src/modules/blog/service.ts` with the following content: +### Test the Scheduled Job - +To test out your scheduled job, start the Medusa application: -```ts title="src/modules/blog/service.ts" highlights={highlights} -import { MedusaService } from "@medusajs/framework/utils" -import Post from "./models/post" +```bash npm2yarn +npm run dev +``` -class BlogModuleService extends MedusaService({ - Post, -}){ -} +After a minute, the following message will be logged to the terminal: -export default BlogModuleService +```bash +info: Greeting! ``` -Your module's service extends a class generated by `MedusaService` from the Modules SDK. This class comes with generated methods for data-management Create, Read, Update, and Delete (CRUD) operations on each of your modules, saving your time that can be spent on building custom business logic. - -The `MedusaService` function accepts an object of data models to generate methods for. You can pass all data models in your module in this object. +*** -For example, the `BlogModuleService` now has a `createPosts` method to create post records, and a `retrievePost` method to retrieve a post record. The suffix of each method (except for `retrieve`) is the pluralized name of the data model. +## Example: Sync Products Once a Day -Find all methods generated by the `MedusaService` in [this reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) +In this section, you'll find a brief example of how you use a scheduled job to sync products to a third-party service. -If a module doesn't have data models, such as when it's integrating a third-party service, it doesn't need to extend `MedusaService`. +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. -### 3. Export Module Definition +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 final piece to a module is its definition, which is exported in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its main service. Medusa will then register the main service in the container under the module's name. +```ts title="src/jobs/sync-products.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp" -So, to export the definition of the Blog Module, create the file `src/modules/blog/index.ts` with the following content: +export default async function syncProductsJob(container: MedusaContainer) { + await syncProductToErpWorkflow(container) + .run() +} - +export const config = { + name: "sync-products-job", + schedule: "0 0 * * *", +} +``` -```ts title="src/modules/blog/index.ts" highlights={moduleDefinitionHighlights} -import BlogModuleService from "./service" -import { Module } from "@medusajs/framework/utils" +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. -export const BLOG_MODULE = "blog" +The next time you start the Medusa application, it will run this job every day at midnight. -export default Module(BLOG_MODULE, { - service: BlogModuleService, -}) -``` -You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: +# Plugins -1. The name that the module's main service is registered under (`blog`). -2. An object with a required property `service` indicating the module's main service. +In this chapter, you'll learn what a plugin is in Medusa. -You export `BLOG_MODULE` to reference the module's name more reliably when resolving its service in other customizations. +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). -### 4. Add Module to Medusa's Configurations +## What is a Plugin? -If you're creating the module in a plugin, this step isn't required as the module is registered when the plugin is registered. Learn more about plugins in [this documentation](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). +A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). -Once you finish building the module, add it to Medusa's configurations to start using it. Medusa will then register the module's main service in the Medusa container, allowing you to resolve and use it in other customizations. +Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project. -In `medusa-config.ts`, add a `modules` property and pass an array with your custom module: + -```ts title="medusa-config.ts" highlights={[["7"]]} -module.exports = defineConfig({ - projectConfig: { - // ... - }, - modules: [ - { - resolve: "./src/modules/blog", - }, - ], -}) -``` +Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md). -Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name. +*** -### 5. Generate Migrations +## Plugin vs Module -Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module. +A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase. -Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. +A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it. -You don't have to write migrations yourself. Medusa's CLI tool has a command that generates the migrations for you. You can also use this command again when you make changes to the module at a later point, and it will generate new migrations for that change. +For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service. -To generate a migration for the Blog Module, run the following command in your Medusa application's directory: +- You want to reuse your Medusa customizations across multiple projects. +- You want to share your Medusa customizations with the community. -If you're creating the module in a plugin, use the [plugin:db:generate command](https://docs.medusajs.com/resources/medusa-cli/commands/plugin#plugindbgenerate/index.html.md) instead. +- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow. -```bash -npx medusa db:generate blog -``` +*** -The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following: +## How to Create a Plugin? -```ts -import { Migration } from "@mikro-orm/migrations" +The next chapter explains how you can create and publish a plugin. -export class Migration20241121103722 extends Migration { +*** - async up(): Promise<void> { - this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));") - } +## Plugin Guides and Resources - async down(): Promise<void> { - this.addSql("drop table if exists \"post\" cascade;") - } +For more resources and guides related to plugins, refer to the [Resources documentation](https://docs.medusajs.com/resources/plugins/index.html.md). -} -``` -In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table. +# Workflows -### 6. Run Migrations +In this chapter, you’ll learn about workflows and how to define and execute them. -To reflect the changes in the generated migration file on the database, run the `db:migrate` command: +## What is a Workflow? -If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in. +In digital commerce you typically have many systems involved in your operations. For example, you may have an ERP system that holds product master data and accounting reports, a CMS system for content, a CRM system for managing customer campaigns, a payment service to process credit cards, and so on. Sometimes you may even have custom built applications that need to participate in the commerce stack. One of the biggest challenges when operating a stack like this is ensuring consistency in the data spread across systems. -```bash -npx medusa db:migrate -``` +Medusa has a built-in durable execution engine to help complete tasks that span multiple systems. You orchestrate your operations across systems in Medusa instead of having to manage it yourself. Other commerce platforms don't have this capability, which makes them a bottleneck to building customizations and iterating quickly. -This creates the `post` table in the database. +A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow similar to how you create a JavaScript function. -*** - -## Test the Module - -Since the module's main service is registered in the Medusa container, you can resolve it in other customizations to use its methods. - -To test out the Blog Module, you'll add the functionality to create a post in a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function that performs a task in a series of steps with rollback logic. Then, you'll expose an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that creates a blog post by executing the workflow. - -By building a commerce feature in a workflow, you can execute it in other customizations while ensuring data consistency across systems. If an error occurs during execution, every step has its own rollback logic to undo its actions. Workflows have other special features which you can learn about in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). - -To create the workflow, create the file `src/workflows/create-post.ts` with the following content: - -```ts title="src/workflows/create-post.ts" highlights={workflowHighlights} -import { - createStep, - createWorkflow, - StepResponse, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { BLOG_MODULE } from "../modules/blog" -import BlogModuleService from "../modules/blog/service" - -type CreatePostWorkflowInput = { - title: string -} - -const createPostStep = createStep( - "create-post", - async ({ title }: CreatePostWorkflowInput, { container }) => { - const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE) - - const post = await blogModuleService.createPosts({ - title, - }) - - return new StepResponse(post, post) - }, - async (post, { container }) => { - const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE) - - await blogModuleService.deletePosts(post.id) - } -) - -export const createPostWorkflow = createWorkflow( - "create-post", - (postInput: CreatePostWorkflowInput) => { - const post = createPostStep(postInput) - - return new WorkflowResponse(post) - } -) -``` - -The workflow has a single step `createPostStep` that creates a post. In the step, you resolve the Blog Module's service from the Medusa container, which the step receives as a parameter. Then, you create the post using the method `createPosts` of the service, which was generated by `MedusaService`. - -The step also has a compensation function, which is a function passed as a third-parameter to `createStep` that implements the logic to rollback the change made by a step in case an error occurs during the workflow's execution. - -You'll now execute that workflow in an API route to expose the feature of creating blog posts to clients. To create an API route, create the file `src/api/blog/posts/route.ts` with the following content: - -```ts -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - createPostWorkflow, -} from "../../../workflows/create-post" - -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - const { result: post } = await createPostWorkflow(req.scope) - .run({ - input: { - title: "My Post", - }, - }) - - res.json({ - post, - }) -} -``` - -This adds a `POST` API route at `/blog/posts`. In the API route, you execute the `createPostWorkflow` by invoking it, passing it the Medusa container in `req.scope`, then invoking the `run` method. In the `run` method, you pass the workflow's input in the `input` property. - -To test this out, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, send a `POST` request to `/blog/posts`: - -```bash -curl -X POST http://localhost:9000/blog/posts -``` - -This will create a post and return it in the response: - -```json -{ - "post": { - "id": "123...", - "title": "My Post", - "created_at": "...", - "updated_at": "..." - } -} -``` - -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. - - -# Scheduled Jobs - -In this chapter, you’ll learn about scheduled jobs and how to use them. - -## What is a Scheduled Job? - -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. - -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. - -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. - -- 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. - -*** - -## 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. - -For example, create the file `src/jobs/hello-world.ts` with the following content: - - - -```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. - - -# Write Integration Tests - -In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests. - -### 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. - -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... - }, -}) - -jest.setTimeout(60 * 1000) -``` - -The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`. - -`testSuite`'s value is a function that defines the tests to run. The function accepts as a parameter an object that has the following properties: - -- `api`: a set of utility methods used to send requests to the Medusa application. It has the following methods: - - `get`: Send a `GET` request to an API route. - - `post`: Send a `POST` request to an API route. - - `delete`: Send a `DELETE` request to an API route. -- `getContainer`: a function that retrieves the Medusa Container. Use the `getContainer().resolve` method to resolve resources from the Medusa Container. - -The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). - -### 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) -``` - -*** - -### Run Tests - -Run the following command to run your tests: - -```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 - -Refer to [this reference in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. - -*** - -## Database Used in Tests - -The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. - -To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md). - -*** - -## Example Integration Tests - -The next chapters provide examples of writing integration tests for API routes and workflows. - - -# Write Tests for Modules - -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) - -## moduleIntegrationTestRunner Utility - -`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. - -For example, assuming you have a `hello` module, create a test file at `src/modules/hello/__tests__/service.spec.ts`: - -```ts title="src/modules/hello/__tests__/service.spec.ts" -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import { HELLO_MODULE } from ".." -import HelloModuleService from "../service" -import MyCustom from "../models/my-custom" - -moduleIntegrationTestRunner<HelloModuleService>({ - moduleName: HELLO_MODULE, - moduleModels: [MyCustom], - resolve: "./src/modules/hello", - testSuite: ({ service }) => { - // TODO write tests - }, -}) - -jest.setTimeout(60 * 1000) -``` - -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 Tests - -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. - -*** - -## 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<HelloModuleService>({ - moduleOptions: { - apiKey: "123", - }, - // ... -}) -``` - -*** - -## Write Tests for Modules without Data Models - -If your module doesn't have a data model, pass a dummy model in the `moduleModels` property. - -For example: - -```ts -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import HelloModuleService from "../service" -import { model } from "@medusajs/framework/utils" - -const DummyModel = model.define("dummy_model", { - id: model.id().primaryKey(), -}) - -moduleIntegrationTestRunner<HelloModuleService>({ - moduleModels: [DummyModel], - // ... -}) - -jest.setTimeout(60 * 1000) -``` - -*** - -### Other Options and Inputs - -Refer to [this reference in the Development Resources documentation](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 [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). - - -# Workflows - -In this chapter, you’ll learn about workflows and how to define and execute them. - -## What is a Workflow? - -In digital commerce you typically have many systems involved in your operations. For example, you may have an ERP system that holds product master data and accounting reports, a CMS system for content, a CRM system for managing customer campaigns, a payment service to process credit cards, and so on. Sometimes you may even have custom built applications that need to participate in the commerce stack. One of the biggest challenges when operating a stack like this is ensuring consistency in the data spread across systems. - -Medusa has a built-in durable execution engine to help complete tasks that span multiple systems. You orchestrate your operations across systems in Medusa instead of having to manage it yourself. Other commerce platforms don't have this capability, which makes them a bottleneck to building customizations and iterating quickly. - -A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow similar to how you create a JavaScript function. - -However, unlike regular functions, workflows: +However, unlike regular functions, workflows: - Create an internal representation of your steps, allowing you to track them and their progress. - Support defining roll-back logic for each step, so that you can handle errors gracefully and your data remain consistent across systems. @@ -2917,1670 +2004,2034 @@ 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. -# Guide: Create Brand API Route - -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. +# Modules -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. +In this chapter, you’ll learn about modules and how to create them. -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 is a Module? -### Prerequisites +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. -- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) +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. -## 1. Create the API Route +Medusa removes this overhead by allowing you to easily write custom modules that integrate into the Medusa application without implications on the existing setup. You can also re-use your modules across Medusa projects. -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…). +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. -Learn more about API routes [in this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md). +- You want to build a custom feature related to a single domain or integrate a third-party service. -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: +- 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). - +*** -```ts title="src/api/admin/brands/route.ts" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - createBrandWorkflow, -} from "../../../workflows/create-brand" +## How to Create a Module? -type PostAdminCreateBrandType = { - name: string -} +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. -export const POST = async ( - req: MedusaRequest<PostAdminCreateBrandType>, - res: MedusaResponse -) => { - const { result } = await createBrandWorkflow(req.scope) - .run({ - input: req.validatedBody, - }) +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 expose an API endpoint to create a blog post. - res.json({ brand: result }) -} -``` +Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/blog`. -You export a route handler function with its name (`POST`) being the HTTP method of the API route you're exposing. +### 1. Create Data Model -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. +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. -`MedusaRequest` accepts the request body's type as a type argument. +You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a `Post` data model in the Blog Module, create the file `src/modules/blog/models/post.ts` with the following content: -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`. + -You return a JSON response with the created brand using the `res.json` method. +```ts title="src/modules/blog/models/post.ts" +import { model } from "@medusajs/framework/utils" -*** +const Post = model.define("post", { + id: model.id().primaryKey(), + title: model.text(), +}) -## 2. Create Validation Schema +export default Post +``` -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. +You define the data model using the `define` method of the DML. It accepts two parameters: -Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. +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. The schema's properties are defined using the `model`'s methods, such as `text` and `id`. + - Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`, so you don't need to add them manually. -Learn more about API route validation in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). +Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/property-types/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: +The code snippet above defines a `Post` data model with `id` and `title` properties. - +### 2. Create Service -```ts title="src/api/admin/brands/validators.ts" -import { z } from "zod" +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. Medusa registers the service in its [container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), allowing you to resolve and use it when building custom commerce flows. -export const PostAdminCreateBrand = z.object({ - name: z.string(), -}) -``` +You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, to create the Blog Module's service, create the file `src/modules/blog/service.ts` with the following content: -You export a validation schema that expects in the request body an object having a `name` property whose value is a string. + -You can then replace `PostAdminCreateBrandType` in `src/api/admin/brands/route.ts` with the following: - -```ts title="src/api/admin/brands/route.ts" -// ... -import { z } from "zod" -import { PostAdminCreateBrand } from "./validators" +```ts title="src/modules/blog/service.ts" highlights={highlights} +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" -type PostAdminCreateBrandType = z.infer<typeof PostAdminCreateBrand> +class BlogModuleService extends MedusaService({ + Post, +}){ +} -// ... +export default BlogModuleService ``` -*** +Your module's service extends a class generated by `MedusaService` from the Modules SDK. This class comes with generated methods for data-management Create, Read, Update, and Delete (CRUD) operations on each of your modules, saving your time that can be spent on building custom business logic. -## 3. Add Validation Middleware +The `MedusaService` function accepts an object of data models to generate methods for. You can pass all data models in your module in this object. -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. +For example, the `BlogModuleService` now has a `createPosts` method to create post records, and a `retrievePost` method to retrieve a post record. The suffix of each method (except for `retrieve`) is the pluralized name of the data model. -Learn more about middlewares in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). +Find all methods generated by the `MedusaService` in [this reference](https://docs.medusajs.com/resources/service-factory-reference/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. +If a module doesn't have data models, such as when it's integrating a third-party service, it doesn't need to extend `MedusaService`. -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: +### 3. Export Module Definition - +The final piece to a module is its definition, which is exported in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its main service. Medusa will then register the main service in the container under the module's name. -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - validateAndTransformBody, -} from "@medusajs/framework/http" -import { PostAdminCreateBrand } from "./admin/brands/validators" +So, to export the definition of the Blog Module, create the file `src/modules/blog/index.ts` with the following content: -export default defineMiddlewares({ - routes: [ - { - matcher: "/admin/brands", - method: "POST", - middlewares: [ - validateAndTransformBody(PostAdminCreateBrand), - ], - }, - ], + + +```ts title="src/modules/blog/index.ts" highlights={moduleDefinitionHighlights} +import BlogModuleService from "./service" +import { Module } from "@medusajs/framework/utils" + +export const BLOG_MODULE = "blog" + +export default Module(BLOG_MODULE, { + service: BlogModuleService, }) ``` -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. +You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: -In the middleware object, you define three properties: +1. The name that the module's main service is registered under (`blog`). +2. An object with a required property `service` indicating the module's main service. -- `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. +You export `BLOG_MODULE` to reference the module's name more reliably when resolving its service in other customizations. -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. +### 4. Add Module to Medusa's Configurations -*** +If you're creating the module in a plugin, this step isn't required as the module is registered when the plugin is registered. Learn more about plugins in [this documentation](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -## Test API Route +Once you finish building the module, add it to Medusa's configurations to start using it. Medusa will then register the module's main service in the Medusa container, allowing you to resolve and use it in other customizations. -To test out the API route, start the Medusa application with the following command: +In `medusa-config.ts`, add a `modules` property and pass an array with your custom module: -```bash npm2yarn -npm run dev +```ts title="medusa-config.ts" highlights={[["7"]]} +module.exports = defineConfig({ + projectConfig: { + // ... + }, + modules: [ + { + resolve: "./src/modules/blog", + }, + ], +}) ``` -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: +Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name. -```bash -curl -X POST 'http://localhost:9000/auth/user/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "admin@medusa-test.com", - "password": "supersecret" -}' -``` +### 5. Generate Migrations -Make sure to replace the email and password with your admin user's credentials. +Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module. -Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). +Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. -Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: +You don't have to write migrations yourself. Medusa's CLI tool has a command that generates the migrations for you. You can also use this command again when you make changes to the module at a later point, and it will generate new migrations for that change. -```bash -curl -X POST 'http://localhost:9000/admin/brands' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data '{ - "name": "Acme" -}' -``` +To generate a migration for the Blog Module, run the following command in your Medusa application's directory: -This returns the created brand in the response: +If you're creating the module in a plugin, use the [plugin:db:generate command](https://docs.medusajs.com/resources/medusa-cli/commands/plugin#plugindbgenerate/index.html.md) instead. -```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" - } -} +```bash +npx medusa db:generate blog ``` -*** +The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following: -## Summary +```ts +import { Migration } from "@mikro-orm/migrations" -By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand. You did that by: +export class Migration20241121103722 extends Migration { -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. + async up(): Promise<void> { + this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));") + } -*** + async down(): Promise<void> { + this.addSql("drop table if exists \"post\" cascade;") + } -## Next Steps: Associate Brand with Product +} +``` -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 the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table. -In the next chapters, you'll learn how to build associations between data models defined in different modules. +### 6. Run Migrations +To reflect the changes in the generated migration file on the database, run the `db:migrate` command: -# Guide: Create Brand Workflow +If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in. -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. +```bash +npx medusa db:migrate +``` -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. +This creates the `post` table in the database. -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. +*** -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). +## Test the Module -### Prerequisites +Since the module's main service is registered in the Medusa container, you can resolve it in other customizations to use its methods. -- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +To test out the Blog Module, you'll add the functionality to create a post in a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function that performs a task in a series of steps with rollback logic. Then, you'll expose an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that creates a blog post by executing the workflow. -*** +By building a commerce feature in a workflow, you can execute it in other customizations while ensuring data consistency across systems. If an error occurs during execution, every step has its own rollback logic to undo its actions. Workflows have other special features which you can learn about in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). -## 1. Create createBrandStep +To create the workflow, create the file `src/workflows/create-post.ts` with the following content: -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 +```ts title="src/workflows/create-post.ts" highlights={workflowHighlights} +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { BLOG_MODULE } from "../modules/blog" +import BlogModuleService from "../modules/blog/service" -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: +type CreatePostWorkflowInput = { + title: string +} - +const createPostStep = createStep( + "create-post", + async ({ title }: CreatePostWorkflowInput, { container }) => { + const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE) -```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" + const post = await blogModuleService.createPosts({ + title, + }) -export type CreateBrandStepInput = { - name: string -} + return new StepResponse(post, post) + }, + async (post, { container }) => { + const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE) -export const createBrandStep = createStep( - "create-brand-step", - async (input: CreateBrandStepInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) + await blogModuleService.deletePosts(post.id) + } +) - const brand = await brandModuleService.createBrands(input) +export const createPostWorkflow = createWorkflow( + "create-post", + (postInput: CreatePostWorkflowInput) => { + const post = createPostStep(postInput) - return new StepResponse(brand, brand.id) + return new WorkflowResponse(post) } ) ``` -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. +The workflow has a single step `createPostStep` that creates a post. In the step, you resolve the Blog Module's service from the Medusa container, which the step receives as a parameter. Then, you create the post using the method `createPosts` of the service, which was generated by `MedusaService`. -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 step also has a compensation function, which is a function passed as a third-parameter to `createStep` that implements the logic to rollback the change made by a step in case an error occurs during the workflow's execution. -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. +You'll now execute that workflow in an API route to expose the feature of creating blog posts to clients. To create an API route, create the file `src/api/blog/posts/route.ts` with the following content: -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. +```ts +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + createPostWorkflow, +} from "../../../workflows/create-post" -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). +export async function POST( + req: MedusaRequest, + res: MedusaResponse +) { + const { result: post } = await createPostWorkflow(req.scope) + .run({ + input: { + title: "My Post", + }, + }) -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. + res.json({ + post, + }) +} +``` -### Add Compensation Function to Step +This adds a `POST` API route at `/blog/posts`. In the API route, you execute the `createPostWorkflow` by invoking it, passing it the Medusa container in `req.scope`, then invoking the `run` method. In the `run` method, you pass the workflow's input in the `input` property. -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. +To test this out, start the Medusa application: -Learn more about the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). +```bash npm2yarn +npm run dev +``` -To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`: +Then, send a `POST` request to `/blog/posts`: -```ts title="src/workflows/create-brand.ts" -export const createBrandStep = createStep( - // ... - async (id: string, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +```bash +curl -X POST http://localhost:9000/blog/posts +``` - await brandModuleService.deleteBrands(id) +This will create a post and return it in the response: + +```json +{ + "post": { + "id": "123...", + "title": "My Post", + "created_at": "...", + "updated_at": "..." } -) +} ``` -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 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. -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. -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). +# Configure Instrumentation -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 this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. + +## Observability with OpenTelemtry + +Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for: + +- HTTP requests +- Workflow executions +- Query usages +- Database queries and operations *** -## 2. Create createBrandWorkflow +## How to Configure Instrumentation in Medusa? -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. +### Prerequisites -Add the following content in the same `src/workflows/create-brand.ts` file: +- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html) -```ts title="src/workflows/create-brand.ts" -// other imports... -import { - // ... - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +### Install Dependencies -// ... +Start by installing the following OpenTelemetry dependencies in your Medusa project: -type CreateBrandWorkflowInput = { - name: string -} +```bash npm2yarn +npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg +``` -export const createBrandWorkflow = createWorkflow( - "create-brand", - (input: CreateBrandWorkflowInput) => { - const brand = createBrandStep(input) +Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies: - return new WorkflowResponse(brand) - } -) +```bash npm2yarn +npm install @opentelemetry/exporter-zipkin ``` -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. +### Add instrumentation.ts -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. +Next, create the file `instrumentation.ts` with the following content: -A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor. +```ts title="instrumentation.ts" +import { registerOtel } from "@medusajs/medusa" +import { ZipkinExporter } from "@opentelemetry/exporter-zipkin" -*** +// If using an exporter other than Zipkin, initialize it here. +const exporter = new ZipkinExporter({ + serviceName: "my-medusa-project", +}) -## Next Steps: Expose Create Brand API Route +export function register() { + registerOtel({ + serviceName: "medusajs", + // pass exporter + exporter, + instrument: { + http: true, + workflows: true, + query: true, + }, + }) +} +``` -You now have a `createBrandWorkflow` that you can execute to create a brand. +In the `instrumentation.ts` file, you export a `register` function that uses Medusa's `registerOtel` utility function. You also initialize an instance of the exporter, such as Zipkin, and pass it to the `registerOtel` function. -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. +`registerOtel` accepts an object where you can pass any [NodeSDKConfiguration](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_node.NodeSDKConfiguration.html) property along with the following properties: +The `NodeSDKConfiguration` properties are accepted since Medusa v2.5.1. -# Create Brands UI Route in Admin +- serviceName: (\`string\`) The name of the service traced. +- exporter: (\[SpanExporter]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_sdk\_trace\_base.SpanExporter.html)) An instance of an exporter, such as Zipkin. +- instrument: (\`object\`) Options specifying what to trace. -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. + - http: (\`boolean\`) Whether to trace HTTP requests. -### Prerequisites + - query: (\`boolean\`) Whether to trace Query usages. -- [Brands Module](https://docs.medusajs.com/learn/customization/custom-features/modules/index.html.md) + - workflows: (\`boolean\`) Whether to trace Workflow executions. -## 1. Get Brands API Route + - db: (\`boolean\`) Whether to trace database queries and operations. +- instrumentations: (\[Instrumentation\[]]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_instrumentation.Instrumentation.html)) Additional instrumentation options that OpenTelemetry accepts. -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. +*** -Replace or create the `GET` API route at `src/api/admin/brands/route.ts` with the following: +## Test it Out -```ts title="src/api/admin/brands/route.ts" highlights={apiRouteHighlights} -// other imports... -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +To test it out, start your exporter, such as Zipkin. -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, - }) +Then, start your Medusa application: - res.json({ - brands, - count, - limit: take, - offset: skip, - }) -} +```bash npm2yarn +npm run dev ``` -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. +Try to open the Medusa Admin or send a request to an API route. -The query configurations are combined from default configurations, which you'll add next, and the request's query parameters: +If you check traces in your exporter, you'll find new traces reported. -- `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. +### Trace Span Names -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: +Trace span names start with the following keywords based on what it's reporting: -- `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. +- `{methodName} {URL}` when reporting HTTP requests, where `{methodName}` is the HTTP method, and `{URL}` is the URL the request is sent to. +- `route:` when reporting route handlers running on an HTTP request. +- `middleware:` when reporting a middleware running on an HTTP request. +- `workflow:` when reporting a workflow execution. +- `step:` when reporting a step in a workflow execution. +- `query.graph:` when reporting Query usages. +- `pg.query:` when reporting database queries and operations. -You return in the response the retrieved brands and the pagination configurations. -Learn more about pagination with Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-pagination/index.html.md). +# Logging -*** +In this chapter, you’ll learn how to use Medusa’s logging utility. -## 2. Add Default Query Configurations +## Logger Class -Next, you'll set the default query configurations of the above API route and allow passing query parameters to change the configurations. +Medusa provides a `Logger` class with advanced logging functionalities. This includes configuring logging levels or saving logs to a file. -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: +The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`. -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - validateAndTransformQuery, -} from "@medusajs/framework/http" -import { createFindParams } from "@medusajs/medusa/api/utils/validators" -// other imports... +*** -export const GetBrandsSchema = createFindParams() +## How to Log a Message -export default defineMiddlewares({ - routes: [ - // ... - { - matcher: "/admin/brands", - method: "GET", - middlewares: [ - validateAndTransformQuery( - GetBrandsSchema, - { - defaults: [ - "id", - "name", - "products.*", - ], - isList: true, - } - ), - ], - }, +Resolve the `logger` using the Medusa container to log a message in your resource. - ], -}) -``` +For example, create the file `src/jobs/log-message.ts` with the following content: -You apply the `validateAndTransformQuery` middleware on the `GET /admin/brands` API route. The middleware accepts two parameters: +```ts title="src/jobs/log-message.ts" highlights={highlights} +import { MedusaContainer } from "@medusajs/framework/types" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -- 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. +export default async function myCustomJob( + container: MedusaContainer +) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) -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. + logger.info("I'm using the logger!") +} -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). +export const config = { + name: "test-logger", + // execute every minute + schedule: "* * * * *", +} +``` -*** +This creates a scheduled job that resolves the `logger` from the Medusa container and uses it to log a message. -## 3. Initialize JS SDK +### Test the Scheduled Job -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. - -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: +To test out the above scheduled job, start the Medusa application: - +```bash npm2yarn +npm run dev +``` -```ts title="src/admin/lib/sdk.ts" -import Medusa from "@medusajs/js-sdk" +After a minute, you'll see the following message as part of the logged messages: -export const sdk = new Medusa({ - baseUrl: import.meta.env.VITE_BACKEND_URL || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", - }, -}) +```text +info: I'm using the logger! ``` -You initialize the SDK passing it the following options: +*** -- `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. +## Log Levels -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). +The `Logger` class has the following methods: -You can now use the SDK to send requests to the Medusa server. +- `info`: The message is logged with level `info`. +- `warn`: The message is logged with level `warn`. +- `error`: The message is logged with level `error`. +- `debug`: The message is logged with level `debug`. -Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). +Each of these methods accepts a string parameter to log in the terminal with the associated level. *** -## 4. Add a UI Route to Show Brands - -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. - -Learn more about UI routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). - -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: +## Logging Configurations - +### Log Level -```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" +The available log levels, from lowest to highest levels, are: -const BrandsPage = () => { - // TODO retrieve brands +1. `silly` (default, meaning messages of all levels are logged) +2. `debug` +3. `info` +4. `warn` +5. `error` - return ( - <Container className="divide-y p-0"> - {/* TODO show brands */} - </Container> - ) -} +You can change that by setting the `LOG_LEVEL` environment variable to the minimum level you want to be logged. -export const config = defineRouteConfig({ - label: "Brands", - icon: TagSolid, -}) +For example: -export default BrandsPage +```bash +LOG_LEVEL=error ``` -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. - -### Retrieve Brands From API Route - -You'll now update the UI route to retrieve the brands from the API route you added earlier. +This logs `error` messages only. -First, add the following type in `src/admin/routes/brands/page.tsx`: +The environment variable must be set as a system environment variable and not in `.env`. -```tsx title="src/admin/routes/brands/page.tsx" -type Brand = { - id: string - name: string -} -type BrandsResponse = { - brands: Brand[] - count: number - limit: number - offset: number -} -``` +### Save Logs in a File -You define the type for a brand, and the type of expected response from the `GET /admin/brands` API route. +Aside from showing the logs in the terminal, you can save the logs in a file by setting the `LOG_FILE` environment variable to the path of the file relative to the Medusa server’s root directory. -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: -```tsx title="src/admin/routes/brands/page.tsx" -import { - // ... - Heading, - createDataTableColumnHelper, - DataTable, - DataTablePaginationState, - useDataTable, -} from "@medusajs/ui" +```bash +LOG_FILE=all.log ``` -You import the `DataTable` component and the following utilities: +Your logs are now saved in the `all.log` file at the root of your Medusa application. -- `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. +The environment variable must be set as a system environment variable and not in `.env`. -You also import the `Heading` component to show a heading above the data table. +*** -Next, you'll define the table's columns. Add the following before the `BrandsPage` component: +## Show Log with Progress -```tsx title="src/admin/routes/brands/page.tsx" -const columnHelper = createDataTableColumnHelper<Brand>() +The `Logger` class has an `activity` method used to log a message of level `info`. If the Medusa application is running in a development environment, a spinner starts to show the activity's progress. -const columns = [ - columnHelper.accessor("id", { - header: "ID", - }), - columnHelper.accessor("name", { - header: "Name", - }), -] -``` +For example: -You use the `createDataTableColumnHelper` utility to create columns for the data table. You define two columns for the ID and name of the brands. +```ts title="src/jobs/log-message.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -Then, replace the `// TODO retrieve brands` in the component with the following: +export default async function myCustomJob( + container: MedusaContainer +) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) -```tsx title="src/admin/routes/brands/page.tsx" highlights={queryHighlights} -const limit = 15 -const [pagination, setPagination] = useState<DataTablePaginationState>({ - pageSize: limit, - pageIndex: 0, -}) -const offset = useMemo(() => { - return pagination.pageIndex * limit -}, [pagination]) + const activityId = logger.activity("First log message") -const { data, isLoading } = useQuery<BrandsResponse>({ - queryFn: () => sdk.client.fetch(`/admin/brands`, { - query: { - limit, - offset, - }, - }), - queryKey: [["brands", limit, offset]], -}) + logger.progress(activityId, `Second log message`) -// TODO configure data table + logger.success(activityId, "Last log message") +} ``` -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: +The `activity` method returns the ID of the started activity. This ID can then be passed to one of the following methods of the `Logger` class: -- `pageSize`: The maximum number of items per page. You set it to `15`. -- `pageIndex`: A zero-based index of the current page of items. +- `progress`: Log a message of level `info` that indicates progress within that same activity. +- `success`: Log a message of level `info` that indicates that the activity has succeeded. This also ends the associated activity. +- `failure`: Log a message of level `error` that indicates that the activity has failed. This also ends the associated activity. -You also define a memoized `offset` value that indicates the number of items to skip before retrieving the current page's items. +If you configured the `LOG_LEVEL` environment variable to a level higher than those associated with the above methods, their messages won’t be logged. -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. -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. +# Medusa Testing Tools -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. +In this chapter, you'll learn about Medusa's testing tools and how to install and configure them. -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. +## @medusajs/test-utils Package -### Display Brands Table +Medusa provides a Testing Framework to create integration tests for your custom API routes, modules, or other Medusa customizations. -Finally, you'll display the brands in a data table. Replace the `// TODO configure data table` in the component with the following: +To use the Testing Framework, install `@medusajs/test-utils` as a `devDependency`: -```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, - }, -}) +```bash npm2yarn +npm install --save-dev @medusajs/test-utils@latest ``` -You use the `useDataTable` hook to initialize and configure the data table. It accepts an object with the following properties: +*** -- `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. +## Install and Configure Jest -Then, replace the `{/* TODO show brands */}` in the return statement with the following: +Writing tests with `@medusajs/test-utils`'s tools requires installing and configuring Jest in your project. -```tsx title="src/admin/routes/brands/page.tsx" -<DataTable instance={table}> - <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center"> - <Heading>Brands</Heading> - </DataTable.Toolbar> - <DataTable.Table /> - <DataTable.Pagination /> -</DataTable> -``` +Run the following command to install the required Jest dependencies: -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. +```bash npm2yarn +npm install --save-dev jest @types/jest @swc/jest +``` -*** +Then, create the file `jest.config.js` with the following content: -## Test it Out +```js title="jest.config.js" +const { loadEnv } = require("@medusajs/framework/utils") +loadEnv("test", process.cwd()) -To test out the UI route, start the Medusa application: +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"], +} -```bash npm2yarn -npm run dev +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"] +} ``` -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. +Next, create the `integration-tests/setup.js` file with the following content: - +```js title="integration-tests/setup.js" +const { MetadataStorage } = require("@mikro-orm/core") + +MetadataStorage.clear() +``` *** -## Summary +## Add Test Commands -By following the previous chapters, you: +Finally, add the following scripts to `package.json`: -- 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. +```json title="package.json" +"scripts": { + // ... + "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit", + "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit", + "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" +}, +``` -*** +You now have two commands: -## Next Steps: Integrate Third-Party Systems +- `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. -Your customizations often span across systems, where you need to retrieve data or perform operations in a third-party system. +Medusa's Testing Framework works for integration tests only. You can write unit tests using Jest. -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. +*** +## Test Tools and Writing Tests -# Guide: Implement Brand Module +The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests. -In this chapter, you'll build a Brand Module that adds a `brand` table to the database and provides data-management features for it. -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. +# Guide: Create Brand API Route -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. +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. -Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +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. -## 1. Create Module Directory +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. -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. +### Prerequisites - +- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) -*** +## 1. Create the API Route -## 2. Create Data Model +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…). -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. +Learn more about API routes [in this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md). -Learn more about data models in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#1-create-data-model/index.html.md). +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: -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: + - +```ts title="src/api/admin/brands/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + createBrandWorkflow, +} from "../../../workflows/create-brand" -```ts title="src/modules/brand/models/brand.ts" -import { model } from "@medusajs/framework/utils" +type PostAdminCreateBrandType = { + name: string +} -export const Brand = model.define("brand", { - id: model.id().primaryKey(), - name: model.text(), -}) -``` +export const POST = async ( + req: MedusaRequest<PostAdminCreateBrandType>, + res: MedusaResponse +) => { + const { result } = await createBrandWorkflow(req.scope) + .run({ + input: req.validatedBody, + }) -You create a `Brand` data model which has an `id` primary key property, and a `name` text property. + res.json({ brand: result }) +} +``` -You define the data model using the `define` method of the DML. It accepts two parameters: +You export a route handler function with its name (`POST`) being the HTTP method of the API route you're exposing. -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. +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. -Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/property-types/index.html.md). +`MedusaRequest` accepts the request body's type as a type argument. -*** +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`. -## 3. Create Module Service +You return a JSON response with the created brand using the `res.json` method. -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. +*** -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. +## 2. Create Validation Schema -Learn more about services in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#2-create-service/index.html.md). +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. -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: +Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. - +Learn more about API route validation in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). -```ts title="src/modules/brand/service.ts" highlights={serviceHighlights} -import { MedusaService } from "@medusajs/framework/utils" -import { Brand } from "./models/brand" +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: -class BrandModuleService extends MedusaService({ - Brand, -}) { + -} +```ts title="src/api/admin/brands/validators.ts" +import { z } from "zod" -export default BrandModuleService +export const PostAdminCreateBrand = z.object({ + name: z.string(), +}) ``` -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. +You export a validation schema that expects in the request body an object having a `name` property whose value is a string. -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 can then replace `PostAdminCreateBrandType` in `src/api/admin/brands/route.ts` with the following: -You'll use these methods in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). +```ts title="src/api/admin/brands/route.ts" +// ... +import { z } from "zod" +import { PostAdminCreateBrand } from "./validators" -Find a reference of all generated methods in [this guide](https://docs.medusajs.com/resources/service-factory-reference/index.html.md). +type PostAdminCreateBrandType = z.infer<typeof PostAdminCreateBrand> + +// ... +``` *** -## 4. Export Module Definition +## 3. Add Validation Middleware -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 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. -So, to export the Brand Module's definition, create the file `src/modules/brand/index.ts` with the following content: +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. -```ts title="src/modules/brand/index.ts" -import { Module } from "@medusajs/framework/utils" -import BrandModuleService from "./service" +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: -export const BRAND_MODULE = "brand" + -export default Module(BRAND_MODULE, { - service: BrandModuleService, +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformBody, +} from "@medusajs/framework/http" +import { PostAdminCreateBrand } from "./admin/brands/validators" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/admin/brands", + method: "POST", + middlewares: [ + validateAndTransformBody(PostAdminCreateBrand), + ], + }, + ], }) ``` -You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: +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. -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. +In the middleware object, you define three properties: -You export `BRAND_MODULE` to reference the module's name more reliably in other customizations. +- `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. -## 5. Add Module to Medusa's Configurations +*** -To start using your module, you must add it to Medusa's configurations in `medusa-config.ts`. +## Test API Route -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`: +To test out the API route, start the Medusa application with the following command: -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/brand", - }, - ], -}) +```bash npm2yarn +npm run dev ``` -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). +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: -## 6. Generate and Run Migrations +```bash +curl -X POST 'http://localhost:9000/auth/user/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "admin@medusa-test.com", + "password": "supersecret" +}' +``` -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. +Make sure to replace the email and password with your admin user's credentials. -Learn more about migrations in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#5-generate-migrations/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). -[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: +Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: ```bash -npx medusa db:generate brand -npx medusa db:migrate +curl -X POST 'http://localhost:9000/admin/brands' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "name": "Acme" +}' ``` -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. +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" + } +} +``` *** -## Next Step: Create Brand Workflow +## Summary -The Brand Module now creates a `brand` table in the database and provides a class to manage its records. +By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand. You did that by: -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. +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. +*** -# Guide: Add Product's Brand Widget in Admin +## Next Steps: Associate Brand with Product -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. +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). -### Prerequisites +In the next chapters, you'll learn how to build associations between data models defined in different modules. -- [Brands linked to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) -## 1. Initialize JS SDK +# Guide: Create Brand Workflow -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. +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. -So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content: +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. -```ts title="src/admin/lib/sdk.ts" -import Medusa from "@medusajs/js-sdk" +Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). -export const sdk = new Medusa({ - baseUrl: import.meta.env.VITE_BACKEND_URL || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", - }, -}) -``` +### Prerequisites -You initialize the SDK passing it the following options: +- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) -- `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). +## 1. Create createBrandStep -You can now use the SDK to send requests to the Medusa server. +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 -Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). +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: -*** + -## 2. Add Widget to Product Details Page +```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" -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. +export type CreateBrandStepInput = { + name: string +} -Learn more about widgets in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md). +export const createBrandStep = createStep( + "create-brand-step", + async (input: CreateBrandStepInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) -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: + const brand = await brandModuleService.createBrands(input) - + return new StepResponse(brand, brand.id) + } +) +``` -```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" +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. -type AdminProductBrand = AdminProduct & { - brand?: { - id: string - name: string - } -} +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. -const ProductBrandWidget = ({ - data: product, -}: DetailWidgetProps<AdminProduct>) => { - 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 +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. - return ( - <Container className="divide-y p-0"> - <div className="flex items-center justify-between px-6 py-4"> - <div> - <Heading level="h2">Brand</Heading> - </div> - </div> - <div - className={clx( - `text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4` - )} - > - <Text size="small" weight="plus" leading="compact"> - Name - </Text> +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. - <Text - size="small" - leading="compact" - className="whitespace-pre-line text-pretty" - > - {brandName || "-"} - </Text> - </div> - </Container> - ) -} +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). -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) +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. -export default ProductBrandWidget -``` +### Add Compensation Function to Step -A widget's file must export: +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. -- 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. +Learn more about the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). -Since the widget is injected at the top of the product details page, the widget receives the product's details as a parameter. +To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`: -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. +```ts title="src/workflows/create-brand.ts" +export const createBrandStep = createStep( + // ... + async (id: string, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + 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. + await brandModuleService.deleteBrands(id) + } +) +``` -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. +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. + +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. + +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. *** -## Test it Out +## 2. Create createBrandWorkflow -To test out your widget, start the Medusa application: +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. -```bash npm2yarn -npm run dev +Add the following content in the same `src/workflows/create-brand.ts` file: + +```ts title="src/workflows/create-brand.ts" +// other imports... +import { + // ... + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +// ... + +type CreateBrandWorkflowInput = { + name: string +} + +export const createBrandWorkflow = createWorkflow( + "create-brand", + (input: CreateBrandWorkflowInput) => { + const brand = createBrandStep(input) + + return new WorkflowResponse(brand) + } +) ``` -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. +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. - +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. + +A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor. *** -## Admin Components Guides +## Next Steps: Expose Create Brand API Route -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. +You now have a `createBrandWorkflow` that you can execute to create a brand. -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. +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. -*** -## Next Chapter: Add UI Route for Brands +# Guide: Implement Brand Module -In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users. +In this chapter, you'll build a Brand Module that adds a `brand` table to the database and provides data-management features for it. +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. -# Guide: Query Product's Brands +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. -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. +Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -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. +## 1. Create Module Directory -### Prerequisites +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. -- [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) + *** -## Approach 1: Retrieve Brands in Existing API Routes - -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. +## 2. Create Data Model -Learn more about selecting fields and relations in the [API Reference](https://docs.medusajs.com/api/admin#select-fields-and-relations). +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. -For example, send the following request to retrieve the list of products with their brands: +Learn more about data models in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#1-create-data-model/index.html.md). -```bash -curl 'http://localhost:9000/admin/products?fields=+brand.*' \ ---header 'Authorization: Bearer {token}' -``` +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: -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). + -Any product that is linked to a brand will have a `brand` property in its object: +```ts title="src/modules/brand/models/brand.ts" +import { model } from "@medusajs/framework/utils" -```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 - } -} +export const Brand = model.define("brand", { + id: model.id().primaryKey(), + name: model.text(), +}) ``` -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. +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: -## Approach 2: Use Query to Retrieve Linked Records +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. -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. +Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/property-types/index.html.md). -Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). +*** -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: +## 3. Create Module Service -```ts title="src/api/admin/brands/route.ts" highlights={highlights} -// other imports... -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +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. -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve("query") - - const { data: brands } = await query.graph({ - entity: "brand", - fields: ["*", "products.*"], - }) +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. - res.json({ brands }) -} -``` +Learn more about services in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#2-create-service/index.html.md). -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: +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: -- `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. + -`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. +```ts title="src/modules/brand/service.ts" highlights={serviceHighlights} +import { MedusaService } from "@medusajs/framework/utils" +import { Brand } from "./models/brand" -### Test it Out +class BrandModuleService extends MedusaService({ + Brand, +}) { -To test the API route out, send a `GET` request to `/admin/brands`: +} -```bash -curl 'http://localhost:9000/admin/brands' \ --H 'Authorization: Bearer {token}' +export default BrandModuleService ``` -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 `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. -This returns the brands in your store with their linked products. For example: +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. -```json title="Example Response" -{ - "brands": [ - { - "id": "123", - // ... - "products": [ - { - "id": "prod_123", - // ... - } - ] - } - ] -} -``` +You'll use these methods in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). + +Find a reference of all generated methods in [this guide](https://docs.medusajs.com/resources/service-factory-reference/index.html.md). *** -## Summary +## 4. Export Module Definition -By following the examples of the previous chapters, you: +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. -- 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. +So, to export the Brand Module's definition, create the file `src/modules/brand/index.ts` with the following content: -*** + -## Next Steps: Customize Medusa Admin +```ts title="src/modules/brand/index.ts" +import { Module } from "@medusajs/framework/utils" +import BrandModuleService from "./service" -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 const BRAND_MODULE = "brand" -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 Module(BRAND_MODULE, { + service: BrandModuleService, +}) +``` +You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: -# Guide: Extend Create Product Flow +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. -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. +You export `BRAND_MODULE` to reference the module's name more reliably in other customizations. -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: +## 5. Add Module to Medusa's Configurations -- 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`. +To start using your module, you must add it to Medusa's configurations in `medusa-config.ts`. -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). +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`: -### Prerequisites +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/brand", + }, + ], +}) +``` -- [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) +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). *** -## 1. Consume the productsCreated Hook +## 6. Generate and Run Migrations -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. +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. -Learn more about the workflow hooks in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). +Learn more about migrations in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#5-generate-migrations/index.html.md). -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. +[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: -To consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content: +```bash +npx medusa db:generate brand +npx medusa db:migrate +``` - +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. -```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" +*** -createProductsWorkflow.hooks.productsCreated( - (async ({ products, additional_data }, { container }) => { - if (!additional_data?.brand_id) { - return new StepResponse([], []) - } +## Next Step: Create Brand Workflow - 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) +The Brand Module now creates a `brand` table in the database and provides a class to manage its records. - // TODO link brand to product - }) -) -``` +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. -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. +# Next.js Starter Storefront -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. +The Medusa application is made up of a Node.js server and an admin dashboard. The storefront is installed and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience. -### Link Brand to Product +The Next.js Starter storefront provides rich commerce features and a sleek design. Developers and businesses can use it as-is or build on top of it to tailor it for the business's unique use case, design, and customer experience. -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. +In this chapter, you’ll learn how to install the Next.js Starter storefront separately from the Medusa application. You can also install it while installing the Medusa application as explained in [the installation chapter](https://docs.medusajs.com/learn/installation/index.html.md). -Learn more about Link in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md). +## Install Next.js Starter -To use Link in the `productsCreated` hook, replace the `TODO` with the following: +### Prerequisites -```ts title="src/workflows/hooks/created-product.ts" highlights={hook2Highlights} -const link = container.resolve("link") -const logger = container.resolve("logger") +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) -const links: LinkDefinition[] = [] +If you already have a Medusa application installed with at least one region, you can install the Next.js Starter storefront with the following steps: -for (const product of products) { - links.push({ - [Modules.PRODUCT]: { - product_id: product.id, - }, - [BRAND_MODULE]: { - brand_id: additional_data.brand_id, - }, - }) -} +1. Clone the [Next.js Starter](https://github.com/medusajs/nextjs-starter-medusa): -await link.create(links) +```bash +git clone https://github.com/medusajs/nextjs-starter-medusa my-medusa-storefront +``` -logger.info("Linked brand to products") +2. Change to the `my-medusa-storefront` directory, install the dependencies, and rename the template environment variable file: -return new StepResponse(links, links) +```bash npm2yarn +cd my-medusa-storefront +npm install +mv .env.template .env.local ``` -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`. +3. Set the Medusa application's publishable API key in the `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY` environment variable. You can retrieve the publishable API key in on the Medusa Admin dashboard by going to Settings -> Publishable API Keys - +```bash +NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_123... +``` -Finally, you return an instance of `StepResponse` returning the created links. +4. While the Medusa application is running, start the Next.js Starter storefront: -### Dismiss Links in Compensation +```bash npm2yarn +npm run dev +``` -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. +Your Next.js Starter storefront is now running at `http://localhost:8000`. -To undo creating the links in the hook, pass the following compensation function as a second parameter to `productsCreated`: +*** -```ts title="src/workflows/hooks/created-product.ts" -createProductsWorkflow.hooks.productsCreated( - // ... - (async (links, { container }) => { - if (!links?.length) { - return - } +## Customize Storefront - const link = container.resolve("link") +To customize the storefront, refer to the following directories: - await link.dismiss(links) - }) -) -``` +- `src/app`: The storefront’s pages. +- `src/modules`: The storefront’s components. +- `src/styles`: The storefront’s styles. -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. +You can learn more about development with Next.js through [their documentation](https://nextjs.org/docs/getting-started). *** -## 2. Configure Additional Data Validation +## Configurations and Integrations -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. +The Next.js Starter is compatible with some Medusa integrations out-of-the-box, such as the Stripe provider module. You can also change some of its configurations if necessary. -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: +Refer to the [Next.js Starter reference](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) for more details. - -```ts title="src/api/middlewares.ts" -import { defineMiddlewares } from "@medusajs/framework/http" -import { z } from "zod" +# 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. -export default defineMiddlewares({ - routes: [ - // ... - { - matcher: "/admin/products", - method: ["POST"], - additionalDataValidator: { - brand_id: z.string().optional(), - }, - }, - ], -}) -``` +### Prerequisites -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/). +- [Brands Module](https://docs.medusajs.com/learn/customization/custom-features/modules/index.html.md) -So, `POST` requests sent to `/admin/products` can now pass the ID of a brand in the `brand_id` property of `additional_data`. +## 1. Get Brands API Route -*** +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. -## Test it Out +Replace or create the `GET` API route at `src/api/admin/brands/route.ts` with the following: -To test it out, first, retrieve the authentication token of your admin user by sending a `POST` request to `/auth/user/emailpass`: +```ts title="src/api/admin/brands/route.ts" highlights={apiRouteHighlights} +// other imports... +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -```bash -curl -X POST 'http://localhost:9000/auth/user/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "admin@medusa-test.com", - "password": "supersecret" -}' +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, + }) +} ``` -Make sure to replace the email and password in the request body with your user's credentials. +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. -Then, send a `POST` request to `/admin/products` to create a product, and pass in the `additional_data` parameter a brand's ID: +The query configurations are combined from default configurations, which you'll add next, and the request's query parameters: -```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}" - } -}' -``` +- `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. -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). +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: -The request creates a product and returns it. +- `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. -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. +You return in the response the retrieved brands and the pagination configurations. + +Learn more about pagination with Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-pagination/index.html.md). *** -## Next Steps: Query Linked Brands and Products +## 2. Add Default Query Configurations -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. +Next, you'll set the default query configurations of the above API route and allow passing query parameters to change the configurations. +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: -# Guide: Define Module Link Between Brand and Product +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformQuery, +} from "@medusajs/framework/http" +import { createFindParams } from "@medusajs/medusa/api/utils/validators" +// other imports... -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. +export const GetBrandsSchema = createFindParams() -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. +export default defineMiddlewares({ + routes: [ + // ... + { + matcher: "/admin/brands", + method: "GET", + middlewares: [ + validateAndTransformQuery( + GetBrandsSchema, + { + defaults: [ + "id", + "name", + "products.*", + ], + isList: true, + } + ), + ], + }, -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. + ], +}) +``` -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. +You apply the `validateAndTransformQuery` middleware on the `GET /admin/brands` API route. The middleware accepts two parameters: -Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). +- 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. -### Prerequisites +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. -- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +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). -## 1. Define Link +*** -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. +## 3. Initialize JS SDK -So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content: +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. - +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/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, +```ts title="src/admin/lib/sdk.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", }, - 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. - -The `defineLink` function accepts two parameters of the same type, which is either: +You initialize the SDK passing it the following options: -- 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. +- `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. -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. +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. -## 2. Sync the Link to the Database +Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). -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 -``` +## 4. Add a UI Route to Show Brands -This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link. +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. -You can also run the `npx medusa db:sync-links` to just sync module links without running migrations. +Learn more about UI routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). -*** +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: -## 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. +```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" +const BrandsPage = () => { + // TODO retrieve brands -# Guide: Sync Brands from Medusa to CMS + return ( + <Container className="divide-y p-0"> + {/* TODO show brands */} + </Container> + ) +} -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. +export const config = defineRouteConfig({ + label: "Brands", + icon: TagSolid, +}) -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. +export default BrandsPage +``` -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. +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. -Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). +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. -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. +### Retrieve Brands From API Route -### Prerequisites +You'll now update the UI route to retrieve the brands from the API route you added earlier. -- [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) +First, add the following type in `src/admin/routes/brands/page.tsx`: -## 1. Emit Event in createBrandWorkflow +```tsx title="src/admin/routes/brands/page.tsx" +type Brand = { + id: string + name: string +} +type BrandsResponse = { + brands: Brand[] + count: number + limit: number + offset: number +} +``` -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. +You define the type for a brand, and the type of expected response from the `GET /admin/brands` API route. -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`: +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`: -```ts title="src/workflows/create-brand.ts" highlights={eventHighlights} -// other imports... +```tsx title="src/admin/routes/brands/page.tsx" import { - emitEventStep, -} from "@medusajs/medusa/core-flows" - -// ... + // ... + Heading, + createDataTableColumnHelper, + DataTable, + DataTablePaginationState, + useDataTable, +} from "@medusajs/ui" +``` -export const createBrandWorkflow = createWorkflow( - "create-brand", - (input: CreateBrandInput) => { - // ... +You import the `DataTable` component and the following utilities: - emitEventStep({ - eventName: "brand.created", - data: { - id: brand.id, - }, - }) +- `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. - return new WorkflowResponse(brand) - } -) -``` +You also import the `Heading` component to show a heading above the data table. -The `emitEventStep` accepts an object parameter having two properties: +Next, you'll define the table's columns. Add the following before the `BrandsPage` component: -- `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. +```tsx title="src/admin/routes/brands/page.tsx" +const columnHelper = createDataTableColumnHelper<Brand>() -You'll learn how to handle this event in a later step. +const columns = [ + columnHelper.accessor("id", { + header: "ID", + }), + columnHelper.accessor("name", { + header: "Name", + }), +] +``` -*** +You use the `createDataTableColumnHelper` utility to create columns for the data table. You define two columns for the ID and name of the brands. -## 2. Create Sync to Third-Party System Workflow +Then, replace the `// TODO retrieve brands` in the component with the following: -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. +```tsx title="src/admin/routes/brands/page.tsx" highlights={queryHighlights} +const limit = 15 +const [pagination, setPagination] = useState<DataTablePaginationState>({ + pageSize: limit, + pageIndex: 0, +}) +const offset = useMemo(() => { + return pagination.pageIndex * limit +}, [pagination]) -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 { data, isLoading } = useQuery<BrandsResponse>({ + queryFn: () => sdk.client.fetch(`/admin/brands`, { + query: { + limit, + offset, + }, + }), + queryKey: [["brands", limit, offset]], +}) -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). +// TODO configure data table +``` -You'll create a `syncBrandToSystemWorkflow` that has two steps: +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: -- `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. +- `pageSize`: The maximum number of items per page. You set it to `15`. +- `pageIndex`: A zero-based index of the current page of items. -### syncBrandToCmsStep +You also define a memoized `offset` value that indicates the number of items to skip before retrieving the current page's items. -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: +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. - +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/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" +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. -type SyncBrandToCmsStepInput = { - brand: InferTypeOf<typeof Brand> -} +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. -const syncBrandToCmsStep = createStep( - "sync-brand-to-cms", - async ({ brand }: SyncBrandToCmsStepInput, { container }) => { - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) +### Display Brands Table - await cmsModuleService.createBrand(brand) +Finally, you'll display the brands in a data table. Replace the `// TODO configure data table` in the component with the following: - return new StepResponse(null, brand.id) +```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, }, - async (id, { container }) => { - if (!id) { - return - } - - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) - - await cmsModuleService.deleteBrand(id) - } -) +}) ``` -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. - -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). +You use the `useDataTable` hook to initialize and configure the data table. It accepts an object with the following properties: -### Create Workflow +- `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. -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: +Then, replace the `{/* TODO show brands */}` in the return statement with the following: -```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" +```tsx title="src/admin/routes/brands/page.tsx" +<DataTable instance={table}> + <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center"> + <Heading>Brands</Heading> + </DataTable.Toolbar> + <DataTable.Table /> + <DataTable.Pagination /> +</DataTable> +``` -// ... +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. -type SyncBrandToCmsWorkflowInput = { - id: string -} +*** -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, - }, - }) +## Test it Out - syncBrandToCmsStep({ - brand: brands[0], - } as SyncBrandToCmsStepInput) +To test out the UI route, start the Medusa application: - return new WorkflowResponse({}) - } -) +```bash npm2yarn +npm run dev ``` -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. - -You'll execute this workflow in the subscriber next. +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. -Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). + *** -## 3. Handle brand.created Event - -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. +## Summary -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: +By following the previous chapters, you: - +- 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. -```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, - }) -} +## Next Steps: Integrate Third-Party Systems -export const config: SubscriberConfig = { - event: "brand.created", -} -``` +Your customizations often span across systems, where you need to retrieve data or perform operations in a third-party system. -A subscriber file must export: +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 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 subscriber function accepts an object parameter that has two properties: +# Guide: Add Product's Brand Widget in Admin -- `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. +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. -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. +### Prerequisites -Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). +- [Brands linked to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) -*** +## 1. Initialize JS SDK -## Test it Out +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. -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. +So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content: -First, start the Medusa application: + + +```ts title="src/admin/lib/sdk.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", + }, +}) +``` + +You initialize the SDK passing it the following options: + +- `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). + +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). + +*** + +## 2. Add Widget to Product Details Page + +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). + +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: + + + +```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" + +type AdminProductBrand = AdminProduct & { + brand?: { + id: string + name: string + } +} + +const ProductBrandWidget = ({ + data: product, +}: DetailWidgetProps<AdminProduct>) => { + 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 ( + <Container className="divide-y p-0"> + <div className="flex items-center justify-between px-6 py-4"> + <div> + <Heading level="h2">Brand</Heading> + </div> + </div> + <div + className={clx( + `text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4` + )} + > + <Text size="small" weight="plus" leading="compact"> + Name + </Text> + + <Text + size="small" + leading="compact" + className="whitespace-pre-line text-pretty" + > + {brandName || "-"} + </Text> + </div> + </Container> + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductBrandWidget +``` + +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. + +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. + +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. + +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. + +*** + +## Test it Out + +To test out your widget, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +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. + + + +*** + +## Admin Components Guides + +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. + +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. + +*** + +## Next Chapter: Add UI Route for Brands + +In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users. + + +# Guide: Define Module Link Between Brand and Product + +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. + +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. + +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. + +Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). + +### Prerequisites + +- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) + +## 1. Define Link + +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. + +So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content: + + + +```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 +) +``` + +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. + +The `defineLink` function accepts two parameters of the same type, which is either: + +- 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. + +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. + +*** + +## 2. Sync the Link to the Database + +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 +``` + +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. + +*** + +## 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. + + +# 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. + +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. + +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. + +Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). + +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. + +### Prerequisites + +- [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 + +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. + +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" + +// ... + +export const createBrandWorkflow = createWorkflow( + "create-brand", + (input: CreateBrandInput) => { + // ... + + emitEventStep({ + eventName: "brand.created", + data: { + id: brand.id, + }, + }) + + return new WorkflowResponse(brand) + } +) +``` + +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. + +You'll learn how to handle this event in a later step. + +*** + +## 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 + +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: + + + +```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" + +type SyncBrandToCmsStepInput = { + brand: InferTypeOf<typeof Brand> +} + +const syncBrandToCmsStep = createStep( + "sync-brand-to-cms", + async ({ brand }: SyncBrandToCmsStepInput, { container }) => { + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) + + await cmsModuleService.createBrand(brand) + + return new StepResponse(null, brand.id) + }, + async (id, { container }) => { + if (!id) { + return + } + + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) + + await cmsModuleService.deleteBrand(id) + } +) +``` + +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. + +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: + +```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" + +// ... + +type SyncBrandToCmsWorkflowInput = { + id: string +} + +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, + }, + }) + + syncBrandToCmsStep({ + brand: brands[0], + } as SyncBrandToCmsStepInput) + + return new WorkflowResponse({}) + } +) +``` + +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. + +You'll execute this workflow in the subscriber next. + +Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). + +*** + +## 3. Handle brand.created Event + +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. + +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/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 config: SubscriberConfig = { + event: "brand.created", +} +``` + +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. + +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. + +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. + +Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). + +*** + +## Test it Out + +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. + +First, start the Medusa application: ```bash npm2yarn npm run dev @@ -4635,51 +4086,6 @@ info: API Key: "123" 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. -# Admin Development Constraints - -This chapter lists some constraints of admin widgets and UI routes. - -## Arrow Functions - -Widget and UI route components must be created as arrow functions. - -```ts highlights={arrowHighlights} -// Don't -function ProductWidget() { - // ... -} - -// Do -const ProductWidget = () => { - // ... -} -``` - -*** - -## Widget Zone - -A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. - -```ts highlights={zoneHighlights} -// Don't -export const config = defineWidgetConfig({ - zone: `product.details.before`, -}) - -// Don't -const ZONE = "product.details.after" -export const config = defineWidgetConfig({ - zone: ZONE, -}) - -// Do -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) -``` - - # Guide: Schedule Syncing Brands from CMS 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. @@ -4989,43 +4395,436 @@ By following the previous chapters, you utilized Medusa's framework and orchestr 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. -# Guide: Integrate CMS Brand System - -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. +# Guide: Query Product's Brands -Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +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. -## 1. Create Module Directory +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. -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. +### Prerequisites - +- [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) *** -## 2. Create Module Service - -Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system. +## Approach 1: Retrieve Brands in Existing API Routes -Create the CMS Module's service at `src/modules/cms/service.ts` with the following content: +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. - +Learn more about selecting fields and relations in the [API Reference](https://docs.medusajs.com/api/admin#select-fields-and-relations). -```ts title="src/modules/cms/service.ts" highlights={serviceHighlights} -import { Logger, ConfigModule } from "@medusajs/framework/types" +For example, send the following request to retrieve the list of products with their brands: -export type ModuleOptions = { - apiKey: string -} +```bash +curl 'http://localhost:9000/admin/products?fields=+brand.*' \ +--header 'Authorization: Bearer {token}' +``` -type InjectedDependencies = { - logger: Logger - configModule: ConfigModule -} +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). -class CmsModuleService { - private options_: ModuleOptions - private logger_: Logger +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. + +*** + +## Approach 2: Use Query to Retrieve Linked Records + +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. + +Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). + +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 const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve("query") + + const { data: brands } = await query.graph({ + entity: "brand", + fields: ["*", "products.*"], + }) + + res.json({ brands }) +} +``` + +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: + +- `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. + +`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. + +### Test it Out + +To test the API route out, send a `GET` request to `/admin/brands`: + +```bash +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). + +This returns the brands in your store with their linked products. For example: + +```json title="Example Response" +{ + "brands": [ + { + "id": "123", + // ... + "products": [ + { + "id": "prod_123", + // ... + } + ] + } + ] +} +``` + +*** + +## Summary + +By following the examples of the previous chapters, you: + +- 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. + +*** + +## Next Steps: Customize Medusa Admin + +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. + +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. + + +# Guide: Extend Create Product Flow + +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. + +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: + +- 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`. + +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 + +- [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) + +*** + +## 1. Consume the productsCreated Hook + +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. + +Learn more about the workflow hooks in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). + +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 consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content: + + + +```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" + +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 + }) +) +``` + +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. + +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. + +### Link Brand to Product + +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. + +Learn more about Link in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md). + +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, + }, + }) +} + +await link.create(links) + +logger.info("Linked brand to products") + +return new StepResponse(links, links) +``` + +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`. + + + +Finally, you return an instance of `StepResponse` returning the created links. + +### Dismiss Links in Compensation + +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. + +To undo creating the links in the hook, pass the following compensation function as a second parameter to `productsCreated`: + +```ts title="src/workflows/hooks/created-product.ts" +createProductsWorkflow.hooks.productsCreated( + // ... + (async (links, { container }) => { + if (!links?.length) { + return + } + + const link = container.resolve("link") + + await link.dismiss(links) + }) +) +``` + +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. + +*** + +## 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. + +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: + + + +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/framework/http" +import { z } from "zod" + +// ... + +export default defineMiddlewares({ + routes: [ + // ... + { + matcher: "/admin/products", + method: ["POST"], + additionalDataValidator: { + brand_id: z.string().optional(), + }, + }, + ], +}) +``` + +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/). + +So, `POST` requests sent to `/admin/products` can now pass the ID of a brand in the `brand_id` property of `additional_data`. + +*** + +## Test it Out + +To test it out, first, retrieve the authentication token of your admin user by sending a `POST` request to `/auth/user/emailpass`: + +```bash +curl -X POST 'http://localhost:9000/auth/user/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "admin@medusa-test.com", + "password": "supersecret" +}' +``` + +Make sure to replace the email and password in the request body with your user's credentials. + +Then, send a `POST` request to `/admin/products` to create a product, and pass in the `additional_data` parameter a brand's ID: + +```bash +curl -X POST 'http://localhost:9000/admin/products' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "title": "Product 1", + "options": [ + { + "title": "Default option", + "values": ["Default option value"] + } + ], + "shipping_profile_id": "{shipping_profile_id}", + "additional_data": { + "brand_id": "{brand_id}" + } +}' +``` + +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. + +*** + +## Next Steps: Query Linked Brands and Products + +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. + + +# Admin Development Constraints + +This chapter lists some constraints of admin widgets and UI routes. + +## Arrow Functions + +Widget and UI route components must be created as arrow functions. + +```ts highlights={arrowHighlights} +// Don't +function ProductWidget() { + // ... +} + +// Do +const ProductWidget = () => { + // ... +} +``` + +*** + +## Widget Zone + +A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. + +```ts highlights={zoneHighlights} +// Don't +export const config = defineWidgetConfig({ + zone: `product.details.before`, +}) + +// Don't +const ZONE = "product.details.after" +export const config = defineWidgetConfig({ + zone: ZONE, +}) + +// Do +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) +``` + + +# Guide: Integrate CMS Brand System + +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). + +## 1. Create Module Directory + +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. + + + +*** + +## 2. Create Module Service + +Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system. + +Create the CMS Module's service at `src/modules/cms/service.ts` with the following content: + + + +```ts title="src/modules/cms/service.ts" highlights={serviceHighlights} +import { Logger, ConfigModule } from "@medusajs/framework/types" + +export type ModuleOptions = { + apiKey: string +} + +type InjectedDependencies = { + logger: Logger + configModule: ConfigModule +} + +class CmsModuleService { + private options_: ModuleOptions + private logger_: Logger constructor({ logger }: InjectedDependencies, options: ModuleOptions) { this.logger_ = logger @@ -5217,135 +5016,276 @@ To check the current environment, Vite exposes two variables: Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode). -# Admin Development Tips - -In this chapter, you'll find some tips for your admin development. - -## Send Requests to API Routes - -To send a request to an API route in the Medusa Application, use 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. - -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. - -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", - }, -}) -``` +# Admin Routing Customizations -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). +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. -Learn more about the JS SDK's configurations [this documentation](https://docs.medusajs.com/resources/js-sdk#js-sdk-configurations/index.html.md). +In this chapter, you'll learn about routing-related customizations that you can use in your admin customizations using React Router. -Then, use the configured SDK with the `useQuery` Tanstack Query hook to send `GET` requests, and `useMutation` hook to send `POST` or `DELETE` requests. +`react-router-dom` is available in your project by default through the Medusa packages. You don't need to install it separately. -For example: +## Link to a Page -### Query +To link to a page in your admin customizations, you can use the `Link` component from `react-router-dom`. For example: -```tsx title="src/admin/widgets/product-widget.ts" highlights={queryHighlights} +```tsx title="src/admin/widgets/product-widget.tsx" highlights={highlights} 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" +import { Container } from "@medusajs/ui" +import { Link } from "react-router-dom" +// The widget const ProductWidget = () => { - const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list(), - queryKey: ["products"], - }) - return ( <Container className="divide-y p-0"> - {isLoading && <span>Loading...</span>} - {data?.products && ( - <ul> - {data.products.map((product) => ( - <li key={product.id}>{product.title}</li> - ))} - </ul> - )} + <Link to={"/orders"}>View Orders</Link> </Container> ) } +// The widget's configurations export const config = defineWidgetConfig({ - zone: "product.list.before", + zone: "product.details.before", }) export default ProductWidget ``` -### Mutation - -```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" +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. -const ProductWidget = ({ - data: productData, -}: DetailWidgetProps<HttpTypes.AdminProduct>) => { - const { mutateAsync } = useMutation({ - mutationFn: (payload: HttpTypes.AdminUpdateProduct) => - sdk.admin.product.update(productData.id, payload), - onSuccess: () => alert("updated product"), - }) +*** - const handleUpdate = () => { - mutateAsync({ - title: "New Product Title", - }) - } - - return ( - <Container className="divide-y p-0"> - <Button onClick={handleUpdate}>Update Title</Button> - </Container> - ) -} +## Admin Route Loader -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) +Route loaders are available starting from Medusa v2.5.1. -export default ProductWidget -``` +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. -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). +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. -### Use Route Loaders for Initial Data +For example, consider the following UI route created at `src/admin/routes/custom/page.tsx`: -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). +```tsx title="src/admin/routes/custom/page.tsx" highlights={loaderHighlights} +import { Container, Heading } from "@medusajs/ui" +import { + useLoaderData, +} from "react-router-dom" -*** +export async function loader() { + // TODO fetch products -## Global Variables in Admin Customizations + return { + products: [], + } +} -In your admin customizations, you can use the following global variables: +const CustomPage = () => { + const { products } = useLoaderData() as Awaited<ReturnType<typeof loader>> -- `__BASE__`: The base path of the Medusa Admin, as set in the [admin.path](https://docs.medusajs.com/resources/references/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/resources/references/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/resources/references/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`. + return ( + <div> + <Container className="divide-y p-0"> + <div className="flex items-center justify-between px-6 py-4"> + <Heading level="h2">Products count: {products.length}</Heading> + </div> + </Container> + </div> + ) +} + +export default CustomPage +``` + +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. + +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 + +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`: + +```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={loaderParamHighlights} +import { Container, Heading } from "@medusajs/ui" +import { + useLoaderData, + LoaderFunctionArgs, +} from "react-router-dom" + +export async function loader({ params }: LoaderFunctionArgs) { + const { id } = params + // TODO fetch product by id + + return { + id, + } +} + +const CustomPage = () => { + const { id } = useLoaderData() as Awaited<ReturnType<typeof loader>> + + return ( + <div> + <Container className="divide-y p-0"> + <div className="flex items-center justify-between px-6 py-4"> + <Heading level="h2">Product ID: {id}</Heading> + </div> + </Container> + </div> + ) +} + +export default CustomPage +``` + +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. + +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. + +### 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. + +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. *** -## Admin Translations +## Other React Router Utilities -The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions. +### Route Handles -Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/resources/contribution-guidelines/admin-translations/index.html.md). +Route handles are available starting from Medusa v2.5.1. + +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: + +```tsx title="src/admin/routes/custom/page.tsx" +export const handle = { + sandbox: true, +} +``` + +### 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. + + +# Admin Widgets + +In this chapter, you’ll learn more about widgets and how to use them. + +## What is an Admin Widget? + +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. + +For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. + +*** + +## How to Create a Widget? + +### Prerequisites + +- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) + +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: + + + +```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" + +// The widget +const ProductWidget = () => { + return ( + <Container className="divide-y p-0"> + <div className="flex items-center justify-between px-6 py-4"> + <Heading level="h2">Product Widget</Heading> + </div> + </Container> + ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +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. + +The widget component must be created as an arrow function. + +### Test the Widget + +To test out the widget, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, open a product’s details page. You’ll find your custom widget at the top of the page. + +*** + +## Props Passed in Detail Pages + +Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. + +For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: + +```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" +import { + DetailWidgetProps, + AdminProduct, +} from "@medusajs/framework/types" + +// The widget +const ProductWidget = ({ + data, +}: DetailWidgetProps<AdminProduct>) => { + return ( + <Container className="divide-y p-0"> + <div className="flex items-center justify-between px-6 py-4"> + <Heading level="h2"> + Product Widget {data.title} + </Heading> + </div> + </Container> + ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +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`. + +*** + +## Injection Zone + +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. + +*** + +## 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. # Admin UI Routes @@ -5584,256 +5524,304 @@ To build admin customizations that match the Medusa Admin's designs and layouts, For more customizations related to routes, refer to the [Routing Customizations chapter](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md). -# Admin Routing Customizations +# Pass Additional Data to Medusa's API Route -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 how to pass additional data in requests to Medusa's API Route. -In this chapter, you'll learn about routing-related customizations that you can use in your admin customizations using React Router. +## Why Pass Additional Data? -`react-router-dom` is available in your project by default through the Medusa packages. You don't need to install it separately. +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. -## Link to a Page +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. -To link to a page in your admin customizations, you can use the `Link` component from `react-router-dom`. 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. -```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" +### API Routes Accepting Additional Data -// The widget -const ProductWidget = () => { - return ( - <Container className="divide-y p-0"> - <Link to={"/orders"}>View Orders</Link> - </Container> - ) -} +### API Routes List -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) +- 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) -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. +## How to Pass Additional Data -*** +### 1. Specify Validation of Additional Data -## Admin Route Loader +Before passing custom data in the `additional_data` object parameter, you must specify validation rules for the allowed properties in the object. -Route loaders are available starting from Medusa v2.5.1. +To do that, use the middleware route object defined in `src/api/middlewares.ts`. -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. +For example, create the file `src/api/middlewares.ts` with the following content: -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. +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/framework/http" +import { z } from "zod" -For example, consider the following UI route created at `src/admin/routes/custom/page.tsx`: +export default defineMiddlewares({ + routes: [ + { + method: "POST", + matcher: "/admin/products", + additionalDataValidator: { + brand: z.string().optional(), + }, + }, + ], +}) +``` -```tsx title="src/admin/routes/custom/page.tsx" highlights={loaderHighlights} -import { Container, Heading } from "@medusajs/ui" -import { - useLoaderData, -} from "react-router-dom" +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. -export async function loader() { - // TODO fetch products +In this example, you indicate that the `additional_data` parameter accepts a `brand` property whose value is an optional string. - return { - products: [], - } -} +Refer to [Zod's documentation](https://zod.dev) for all available validation rules. -const CustomPage = () => { - const { products } = useLoaderData() as Awaited<ReturnType<typeof loader>> +### 2. Pass the Additional Data in a Request - return ( - <div> - <Container className="divide-y p-0"> - <div className="flex items-center justify-between px-6 py-4"> - <Heading level="h2">Products count: {products.length}</Heading> - </div> - </Container> - </div> - ) -} +You can now pass a `brand` property in the `additional_data` parameter of a request to the Create Product API Route. -export default CustomPage +For example: + +```bash +curl -X POST 'http://localhost:9000/admin/products' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "title": "Product 1", + "options": [ + { + "title": "Default option", + "values": ["Default option value"] + } + ], + "shipping_profile_id": "{shipping_profile_id}", + "additional_data": { + "brand": "Acme" + } +}' ``` -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. +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. -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. +In this request, you pass in the `additional_data` parameter a `brand` property and set its value to `Acme`. -### Route Parameters +The `additional_data` is then passed to hooks in the `createProductsWorkflow` used by the API route. -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`: +*** -```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={loaderParamHighlights} -import { Container, Heading } from "@medusajs/ui" -import { - useLoaderData, - LoaderFunctionArgs, -} from "react-router-dom" +## Use Additional Data in a Hook -export async function loader({ params }: LoaderFunctionArgs) { - const { id } = params - // TODO fetch product by id +Learn about workflow hooks in [this guide](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). - return { - id, - } -} +Step functions consuming the workflow hook can access the `additional_data` in the first parameter. -const CustomPage = () => { - const { id } = useLoaderData() as Awaited<ReturnType<typeof loader>> +For example, consider you want to store the data passed in `additional_data` in the product's `metadata` property. - return ( - <div> - <Container className="divide-y p-0"> - <div className="flex items-center justify-between px-6 py-4"> - <Heading level="h2">Product ID: {id}</Heading> - </div> - </Container> - </div> - ) -} +To do that, create the file `src/workflows/hooks/product-created.ts` with the following content: -export default CustomPage -``` +```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" -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. +createProductsWorkflow.hooks.productsCreated( + async ({ products, additional_data }, { container }) => { + if (!additional_data?.brand) { + return + } -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. + const productModuleService = container.resolve( + Modules.PRODUCT + ) -### When to Use Route Loaders + await productModuleService.upsertProducts( + products.map((product) => ({ + ...product, + metadata: { + ...product.metadata, + brand: additional_data.brand, + }, + })) + ) -A route loader is executed before the route is loaded. So, it will block navigation until the loader function is resolved. + return new StepResponse(products, { + products, + additional_data, + }) + } +) +``` -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. +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. -## Other React Router Utilities +### Compensation Function -### Route Handles +Hooks also accept a compensation function as a second parameter to undo the actions made by the step function. -Route handles are available starting from Medusa v2.5.1. +For example, pass the following second parameter to the `productsCreated` hook: -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. +```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 + } -For example: + const productModuleService = container.resolve( + Modules.PRODUCT + ) -```tsx title="src/admin/routes/custom/page.tsx" -export const handle = { - sandbox: true, -} + await productModuleService.upsertProducts( + products + ) + } +) ``` -### React Router Components and Hooks +This updates the products to their original state before adding the brand to their `metadata` property. -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. +# Admin Development Tips -# Admin Widgets +In this chapter, you'll find some tips for your admin development. -In this chapter, you’ll learn more about widgets and how to use them. +## Send Requests to API Routes -## What is an Admin Widget? +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. -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. +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. -For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. +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" -## How to Create a Widget? +export const sdk = new Medusa({ + baseUrl: import.meta.env.VITE_BACKEND_URL || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, +}) +``` -### Prerequisites +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). -- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) +Learn more about the JS SDK's configurations [this documentation](https://docs.medusajs.com/resources/js-sdk#js-sdk-configurations/index.html.md). -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. +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, create the file `src/admin/widgets/product-widget.tsx` with the following content: +For example: - +### Query -```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} +```tsx title="src/admin/widgets/product-widget.ts" highlights={queryHighlights} import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" +import { Button, Container } from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { sdk } from "../lib/config" +import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" -// The widget const ProductWidget = () => { - return ( - <Container className="divide-y p-0"> - <div className="flex items-center justify-between px-6 py-4"> - <Heading level="h2">Product Widget</Heading> - </div> + const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list(), + queryKey: ["products"], + }) + + return ( + <Container className="divide-y p-0"> + {isLoading && <span>Loading...</span>} + {data?.products && ( + <ul> + {data.products.map((product) => ( + <li key={product.id}>{product.title}</li> + ))} + </ul> + )} </Container> ) } -// The widget's configurations export const config = defineWidgetConfig({ - zone: "product.details.before", + zone: "product.list.before", }) export default ProductWidget ``` -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. - -The widget component must be created as an arrow function. - -### Test the Widget - -To test out the widget, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, open a product’s details page. You’ll find your custom widget at the top of the page. - -*** - -## Props Passed in Detail Pages - -Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. - -For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: +### Mutation -```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights} +```tsx title="src/admin/widgets/product-widget.ts" highlights={mutationHighlights} import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" -import { - DetailWidgetProps, - AdminProduct, -} from "@medusajs/framework/types" +import { Button, Container } from "@medusajs/ui" +import { useMutation } from "@tanstack/react-query" +import { sdk } from "../lib/config" +import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" -// The widget const ProductWidget = ({ - data, -}: DetailWidgetProps<AdminProduct>) => { + data: productData, +}: DetailWidgetProps<HttpTypes.AdminProduct>) => { + const { mutateAsync } = useMutation({ + mutationFn: (payload: HttpTypes.AdminUpdateProduct) => + sdk.admin.product.update(productData.id, payload), + onSuccess: () => alert("updated product"), + }) + + const handleUpdate = () => { + mutateAsync({ + title: "New Product Title", + }) + } + return ( <Container className="divide-y p-0"> - <div className="flex items-center justify-between px-6 py-4"> - <Heading level="h2"> - Product Widget {data.title} - </Heading> - </div> + <Button onClick={handleUpdate}>Update Title</Button> </Container> ) } -// The widget's configurations export const config = defineWidgetConfig({ zone: "product.details.before", }) @@ -5841,19 +5829,136 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` -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`. +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 + +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). *** -## Injection Zone +## Global Variables in Admin Customizations -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. +In your admin customizations, you can use the following global variables: + +- `__BASE__`: The base path of the Medusa Admin, as set in the [admin.path](https://docs.medusajs.com/resources/references/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/resources/references/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/resources/references/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`. *** -## Admin Components List +## Admin Translations -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. +The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions. + +Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/resources/contribution-guidelines/admin-translations/index.html.md). + + +# Throwing and Handling Errors + +In this guide, you'll learn how to throw errors in your Medusa application, how it affects an API route's response, and how to change the default error handler of your Medusa application. + +## Throw MedusaError + +When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError` from the Medusa Framework. + +The Medusa application's API route error handler then wraps your thrown error in a uniform object and returns it in the response. + +For example: + +```ts +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { MedusaError } from "@medusajs/framework/utils" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + if (!req.query.q) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "The `q` query parameter is required." + ) + } + + // ... +} +``` + +The `MedusaError` class accepts in its constructor two parameters: + +1. The first is the error's type. `MedusaError` has a static property `Types` that you can use. `Types` is an enum whose possible values are explained in the next section. +2. The second is the message to show in the error response. + +### Error Object in Response + +The error object returned in the response has two properties: + +- `type`: The error's type. +- `message`: The error message, if available. +- `code`: A common snake-case code. Its values can be: + - `invalid_request_error` for the `DUPLICATE_ERROR` type. + - `api_error`: for the `DB_ERROR` type. + - `invalid_state_error` for `CONFLICT` error type. + - `unknown_error` for any unidentified error type. + - For other error types, this property won't be available unless you provide a code as a third parameter to the `MedusaError` constructor. + +### MedusaError Types + +|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\`| + +*** + +## Override Error Handler + +The `defineMiddlewares` function used to apply middlewares on routes accepts an `errorHandler` in its object parameter. Use it to override the default error handler for API routes. + +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.", + }) + }, +}) +``` + +The `errorHandler` property's value is a function that accepts four parameters: + +1. The error thrown. Its type can be `MedusaError` or any other thrown error type. +2. A request object of type `MedusaRequest`. +3. A response object of type `MedusaResponse`. +4. A function of type MedusaNextFunction that executes the next middleware in the stack. + +This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message. # Handling CORS in API Routes @@ -5968,375 +6073,214 @@ export default defineMiddlewares({ This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`. -# Pass Additional Data to Medusa's API Route +# HTTP Methods -In this chapter, you'll learn how to pass additional data in requests to Medusa's API Route. +In this chapter, you'll learn about how to add new API routes for each HTTP method. -## Why Pass Additional Data? +## HTTP Method Handler -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. +An API route is created for every HTTP method you export a handler function for in a route file. -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. +Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`. -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. +For example, create the file `src/api/hello-world/route.ts` with the following content: -### API Routes Accepting Additional Data +```ts title="src/api/hello-world/route.ts" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -### API Routes List +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "[GET] Hello world!", + }) +} -- 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) +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "[POST] Hello world!", + }) +} +``` -*** +This adds two API Routes: -## How to Pass Additional Data +- A `GET` route at `http://localhost:9000/hello-world`. +- A `POST` route at `http://localhost:9000/hello-world`. -### 1. Specify Validation of Additional Data -Before passing custom data in the `additional_data` object parameter, you must specify validation rules for the allowed properties in the object. +# API Route Parameters -To do that, use the middleware route object defined in `src/api/middlewares.ts`. +In this chapter, you’ll learn about path, query, and request body parameters. -For example, create the file `src/api/middlewares.ts` with the following content: +## Path Parameters -```ts title="src/api/middlewares.ts" -import { defineMiddlewares } from "@medusajs/framework/http" -import { z } from "zod" +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]`. -export default defineMiddlewares({ - routes: [ - { - method: "POST", - matcher: "/admin/products", - additionalDataValidator: { - brand: z.string().optional(), - }, - }, - ], -}) +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" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[GET] Hello ${req.params.id}!`, + }) +} ``` -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 `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs. -In this example, you indicate that the `additional_data` parameter accepts a `brand` property whose value is an optional string. +### Multiple Path Parameters -Refer to [Zod's documentation](https://zod.dev) for all available validation rules. +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]`. -### 2. Pass the Additional Data in a Request +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: -You can now pass a `brand` property in the `additional_data` parameter of a request to the Create Product API Route. +```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[GET] Hello ${ + req.params.id + } - ${req.params.name}!`, + }) +} +``` + +You access the `id` and `name` path parameters using the `req.params` property. + +*** + +## Query Parameters + +You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value. For example: -```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" - } -}' +```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}`, + }) +} ``` -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. +The value of `req.query.name` is the value passed in `?name=John`, for example. -In this request, you pass in the `additional_data` parameter a `brand` property and set its value to `Acme`. +### Validate Query Parameters -The `additional_data` is then passed to hooks in the `createProductsWorkflow` used by the API route. +You can apply validation rules on received query 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-query-paramters/index.html.md). *** -## Use Additional Data in a Hook +## Request Body Parameters -Learn about workflow hooks in [this guide](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). +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. -Step functions consuming the workflow hook can access the `additional_data` in the first parameter. +Learn more about configuring body parsing in [this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md). -For example, consider you want to store the data passed in `additional_data` in the product's `metadata` property. +For example: -To do that, create the file `src/workflows/hooks/product-created.ts` with the following content: +```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -```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" +type HelloWorldReq = { + name: string +} -createProductsWorkflow.hooks.productsCreated( - async ({ products, additional_data }, { container }) => { - if (!additional_data?.brand) { - return - } +export const POST = async ( + req: MedusaRequest<HelloWorldReq>, + res: MedusaResponse +) => { + res.json({ + message: `[POST] Hello ${req.body.name}!`, + }) +} +``` - const productModuleService = container.resolve( - Modules.PRODUCT - ) +In this example, you use the `name` request body parameter to create the message in the returned response. - await productModuleService.upsertProducts( - products.map((product) => ({ - ...product, - metadata: { - ...product.metadata, - brand: additional_data.brand, - }, - })) - ) +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. - return new StepResponse(products, { - products, - additional_data, - }) - } -) +To test it out, send the following request to your Medusa application: + +```bash +curl -X POST 'http://localhost:9000/hello-world' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "name": "John" +}' ``` -This consumes the `productsCreated` hook, which runs after the products are created. +This returns the following JSON object: -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. +```json +{ + "message": "[POST] Hello John!" +} +``` -### Compensation Function +### Validate Body Parameters -Hooks also accept a compensation function as a second parameter to undo the actions made by the step function. +You can apply validation rules on received body parameters to ensure they match specified rules and types. -For example, pass the following second parameter to the `productsCreated` hook: +Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md). -```ts title="src/workflows/hooks/product-created.ts" -createProductsWorkflow.hooks.productsCreated( - async ({ products, additional_data }, { container }) => { - // ... - }, - async ({ products, additional_data }, { container }) => { - if (!additional_data.brand) { - return - } - const productModuleService = container.resolve( - Modules.PRODUCT - ) +# Configure Request Body Parser - await productModuleService.upsertProducts( - products - ) - } -) -``` +In this chapter, you'll learn how to configure the request body parser for your API routes. -This updates the products to their original state before adding the brand to their `metadata` property. +## Default Body Parser Configuration +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. -# Throwing and Handling Errors +This chapter shares some examples of configuring the body parser for different data types or use cases. -In this guide, you'll learn how to throw errors in your Medusa application, how it affects an API route's response, and how to change the default error handler of your Medusa application. +*** -## Throw MedusaError +## Preserve Raw Body Data for Webhooks -When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError` from the Medusa Framework. +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. -The Medusa application's API route error handler then wraps your thrown error in a uniform object and returns it in the response. +To do that, create the file `src/api/middlewares.ts` with the following content: -For example: - -```ts -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { MedusaError } from "@medusajs/framework/utils" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - if (!req.query.q) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "The `q` query parameter is required." - ) - } - - // ... -} -``` - -The `MedusaError` class accepts in its constructor two parameters: - -1. The first is the error's type. `MedusaError` has a static property `Types` that you can use. `Types` is an enum whose possible values are explained in the next section. -2. The second is the message to show in the error response. - -### Error Object in Response - -The error object returned in the response has two properties: - -- `type`: The error's type. -- `message`: The error message, if available. -- `code`: A common snake-case code. Its values can be: - - `invalid_request_error` for the `DUPLICATE_ERROR` type. - - `api_error`: for the `DB_ERROR` type. - - `invalid_state_error` for `CONFLICT` error type. - - `unknown_error` for any unidentified error type. - - For other error types, this property won't be available unless you provide a code as a third parameter to the `MedusaError` constructor. - -### MedusaError Types - -|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\`| - -*** - -## Override Error Handler - -The `defineMiddlewares` function used to apply middlewares on routes accepts an `errorHandler` in its object parameter. Use it to override the default error handler for API routes. - -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.", - }) - }, -}) -``` - -The `errorHandler` property's value is a function that accepts four parameters: - -1. The error thrown. Its type can be `MedusaError` or any other thrown error type. -2. A request object of type `MedusaRequest`. -3. A response object of type `MedusaResponse`. -4. A function of type MedusaNextFunction that executes the next middleware in the stack. - -This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message. - - -# HTTP Methods - -In this chapter, you'll learn about how to add new API routes for each HTTP method. - -## HTTP Method Handler - -An API route is created for every HTTP method you export a handler function for in a route file. - -Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`. - -For example, create the file `src/api/hello-world/route.ts` with the following content: - -```ts title="src/api/hello-world/route.ts" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "[GET] Hello world!", - }) -} - -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "[POST] Hello world!", - }) -} -``` - -This adds two API Routes: - -- A `GET` route at `http://localhost:9000/hello-world`. -- A `POST` route at `http://localhost:9000/hello-world`. - - -# Configure Request Body Parser - -In this chapter, you'll learn how to configure the request body parser for your API routes. - -## Default Body Parser Configuration - -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. - -*** - -## Preserve Raw Body Data for Webhooks - -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: - -```ts title="src/api/middlewares.ts" highlights={preserveHighlights} -import { defineMiddlewares } from "@medusajs/framework/http" +```ts title="src/api/middlewares.ts" highlights={preserveHighlights} +import { defineMiddlewares } from "@medusajs/framework/http" export default defineMiddlewares({ routes: [ @@ -6488,151 +6432,6 @@ export async function POST( 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. -# API Route Parameters - -In this chapter, you’ll learn about path, query, and request body parameters. - -## Path Parameters - -To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`. - -For example, to create an API Route at the path `/hello-world/:id`, where `:id` is a path parameter, create the file `src/api/hello-world/[id]/route.ts` with the following content: - -```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[GET] Hello ${req.params.id}!`, - }) -} -``` - -The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs. - -### Multiple Path Parameters - -To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`. - -For example, to create an API route at `/hello-world/:id/name/:name`, create the file `src/api/hello-world/[id]/name/[name]/route.ts` with the following content: - -```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[GET] Hello ${ - req.params.id - } - ${req.params.name}!`, - }) -} -``` - -You access the `id` and `name` path parameters using the `req.params` property. - -*** - -## Query Parameters - -You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value. - -For example: - -```ts title="src/api/hello-world/route.ts" highlights={queryHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `Hello ${req.query.name}`, - }) -} -``` - -The value of `req.query.name` is the value passed in `?name=John`, for example. - -### Validate Query Parameters - -You can apply validation rules on received query 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-query-paramters/index.html.md). - -*** - -## Request Body Parameters - -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. - -Learn more about configuring body parsing in [this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md). - -For example: - -```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -type HelloWorldReq = { - name: string -} - -export const POST = async ( - req: MedusaRequest<HelloWorldReq>, - res: MedusaResponse -) => { - res.json({ - message: `[POST] Hello ${req.body.name}!`, - }) -} -``` - -In this example, you use the `name` request body parameter to create the message in the returned response. - -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: - -```bash -curl -X POST 'http://localhost:9000/hello-world' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "name": "John" -}' -``` - -This returns the following JSON object: - -```json -{ - "message": "[POST] Hello John!" -} -``` - -### Validate Body Parameters - -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). - - # Middlewares In this chapter, you’ll learn about middlewares and how to create them. @@ -6906,12 +6705,12 @@ export default defineMiddlewares({ }, { matcher: "/custom*", - method: ["GET"] + method: ["GET"], middlewares: [/* ... */], }, { matcher: "/custom/:id", - method: ["GET"] + method: ["GET"], middlewares: [/* ... */], }, ], @@ -7141,194 +6940,6 @@ export const GET = async ( In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user. -# Seed Data with Custom CLI Script - -In this chapter, you'll learn how to seed data using a custom CLI script. - -## How to Seed Data - -To seed dummy data for development or demo purposes, use a custom CLI script. - -In the CLI script, use your custom workflows or Medusa's existing workflows, which you can browse in [this reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md), to seed data. - -### Example: Seed Dummy Products - -In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products. - -First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script: - -```bash npm2yarn -npm install --save-dev @faker-js/faker -``` - -Then, create the file `src/scripts/demo-products.ts` with the following content: - -```ts title="src/scripts/demo-products.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" -import { ExecArgs } from "@medusajs/framework/types" -import { faker } from "@faker-js/faker" -import { - ContainerRegistrationKeys, - Modules, - ProductStatus, -} from "@medusajs/framework/utils" -import { - createInventoryLevelsWorkflow, - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -export default async function seedDummyProducts({ - container, -}: ExecArgs) { - const salesChannelModuleService = container.resolve( - Modules.SALES_CHANNEL - ) - const logger = container.resolve( - ContainerRegistrationKeys.LOGGER - ) - const query = container.resolve( - ContainerRegistrationKeys.QUERY - ) - - const defaultSalesChannel = await salesChannelModuleService - .listSalesChannels({ - name: "Default Sales Channel", - }) - - const sizeOptions = ["S", "M", "L", "XL"] - const colorOptions = ["Black", "White"] - const currency_code = "eur" - const productsNum = 50 - - // TODO seed products -} -``` - -So far, in the script, you: - -- Resolve the Sales Channel Module's main service to retrieve the application's default sales channel. This is the sales channel the dummy products will be available in. -- Resolve the Logger to log messages in the terminal, and Query to later retrieve data useful for the seeded products. -- Initialize some default data to use when seeding the products next. - -Next, replace the `TODO` with the following: - -```ts title="src/scripts/demo-products.ts" -const productsData = new Array(productsNum).fill(0).map((_, index) => { - const title = faker.commerce.product() + "_" + index - return { - title, - is_giftcard: true, - description: faker.commerce.productDescription(), - status: ProductStatus.PUBLISHED, - options: [ - { - title: "Size", - values: sizeOptions, - }, - { - title: "Color", - values: colorOptions, - }, - ], - images: [ - { - url: faker.image.urlPlaceholder({ - text: title, - }), - }, - { - url: faker.image.urlPlaceholder({ - text: title, - }), - }, - ], - variants: new Array(10).fill(0).map((_, variantIndex) => ({ - title: `${title} ${variantIndex}`, - sku: `variant-${variantIndex}${index}`, - prices: new Array(10).fill(0).map((_, priceIndex) => ({ - currency_code, - amount: 10 * priceIndex, - })), - options: { - Size: sizeOptions[Math.floor(Math.random() * 3)], - }, - })), - shipping_profile_id: "sp_123", - sales_channels: [ - { - id: defaultSalesChannel[0].id, - }, - ], - } -}) - -// TODO seed products -``` - -You generate fifty products using the sales channel and variables you initialized, and using Faker for random data, such as the product's title or images. - -Then, replace the new `TODO` with the following: - -```ts title="src/scripts/demo-products.ts" -const { result: products } = await createProductsWorkflow(container).run({ - input: { - products: productsData, - }, -}) - -logger.info(`Seeded ${products.length} products.`) - -// TODO add inventory levels -``` - -You create the generated products using the `createProductsWorkflow` imported previously from `@medusajs/medusa/core-flows`. It accepts the product data as input, and returns the created products. - -Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following: - -```ts title="src/scripts/demo-products.ts" -logger.info("Seeding inventory levels.") - -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: ["id"], -}) - -const { data: inventoryItems } = await query.graph({ - entity: "inventory_item", - fields: ["id"], -}) - -const inventoryLevels = inventoryItems.map((inventoryItem) => ({ - location_id: stockLocations[0].id, - stocked_quantity: 1000000, - inventory_item_id: inventoryItem.id, -})) - -await createInventoryLevelsWorkflow(container).run({ - input: { - inventory_levels: inventoryLevels, - }, -}) - -logger.info("Finished seeding inventory levels data.") -``` - -You use Query to retrieve the stock location, to use the first location in the application, and the inventory items. - -Then, you generate inventory levels for each inventory item, associating it with the first stock location. - -Finally, you use the `createInventoryLevelsWorkflow` from Medusa's core workflows to create the inventory levels. - -### Test Script - -To test out the script, run the following command in your project's directory: - -```bash -npx medusa exec ./src/scripts/demo-products.ts -``` - -This seeds the products to your database. If you run your Medusa application and view the products in the dashboard, you'll find fifty new products. - - # 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. @@ -7578,6 +7189,51 @@ For example, if you omit the `a` parameter, you'll receive a `400` response code To see different examples and learn more about creating a validation schema, refer to [Zod's documentation](https://zod.dev). +# Event Data Payload + +In this chapter, you'll learn how subscribers receive an event's data payload. + +## Access Event's Data Payload + +When events are emitted, they’re emitted with a data payload. + +The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context. + +For example: + +```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" + +export default async function productCreateHandler({ + event, +}: SubscriberArgs<{ id: string }>) { + const productId = event.data.id + console.log(`The product ${productId} was created`) +} + +export const config: SubscriberConfig = { + event: "product.created", +} +``` + +The `event` object has the following properties: + +- 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. + +This logs the product ID received in the `product.created` event’s data payload to the console. + +{/* --- + +## List of Events with Data Payload + +Refer to [this reference](!resources!/events-reference) for a full list of events emitted by Medusa and their data payloads. */} + + # API Route Response In this chapter, you'll learn how to send a response in your API route. @@ -7680,191 +7336,153 @@ This API route opens a stream by setting the `Content-Type` in the header to `te 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. -# Event Data Payload - -In this chapter, you'll learn how subscribers receive an event's data payload. - -## Access Event's Data Payload +# Add Columns to a Link -When events are emitted, they’re emitted with a data payload. +In this chapter, you'll learn how to add custom columns to a link definition and manage them. -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. +## How to Add Custom Columns to a Link's Table? -For example: +The `defineLink` function used to define a link accepts a third parameter, which is an object of options. -```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" +To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property: -export default async function productCreateHandler({ - event, -}: SubscriberArgs<{ id: string }>) { - const productId = event.data.id - console.log(`The product ${productId} was created`) -} +```ts highlights={linkHighlights} +import HelloModule from "../modules/hello" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" -export const config: SubscriberConfig = { - event: "product.created", -} +export default defineLink( + ProductModule.linkable.product, + HelloModule.linkable.myCustom, + { + database: { + extraColumns: { + metadata: { + type: "json", + }, + }, + }, + } +) ``` -The `event` object has the following properties: - -- 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. - -This logs the product ID received in the `product.created` event’s data payload to the console. - -{/* --- - -## List of Events with Data Payload - -Refer to [this reference](!resources!/events-reference) for a full list of events emitted by Medusa and their data payloads. */} - - -# Configure Data Model Properties - -In this chapter, you’ll learn how to configure data model properties. +This adds to the table created for the link between `product` and `myCustom` a `metadata` column of type `json`. -## Property’s Default Value +### Database Options -Use the `default` method on a property's definition to specify the default value of a property. +The `database` property defines configuration for the table created in the database. -For example: +Its `extraColumns` property defines custom columns to create in the link's table. -```ts highlights={defaultHighlights} -import { model } from "@medusajs/framework/utils" +`extraColumns`'s value is an object whose keys are the names of the columns, and values are the column's configurations as an object. -const MyCustom = model.define("my_custom", { - color: model - .enum(["black", "white"]) - .default("black"), - age: model - .number() - .default(0), - // ... -}) +### Column Configurations -export default MyCustom -``` +The column's configurations object accepts the following properties: -In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`. +- `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. *** -## Nullable Property +## Set Custom Column when Creating Link -Use the `nullable` method to indicate that a property’s value can be `null`. +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={nullableHighlights} -import { model } from "@medusajs/framework/utils" +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 MyCustom = model.define("my_custom", { - price: model.bigNumber().nullable(), - // ... +```ts +await link.create({ + [Modules.PRODUCT]: { + product_id: "123", + }, + HELLO_MODULE: { + my_custom_id: "321", + }, + data: { + metadata: { + test: true, + }, + }, }) - -export default MyCustom ``` *** -## Unique Property +## Retrieve Custom Column with Link -The `unique` method indicates that a property’s value must be unique in the database through a unique index. +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 highlights={uniqueHighlights} -import { model } from "@medusajs/framework/utils" +```ts highlights={retrieveHighlights} +import productHelloLink from "../links/product-hello" -const User = model.define("user", { - email: model.text().unique(), - // ... -}) +// ... -export default User +const { data } = await query.graph({ + entity: productHelloLink.entryPoint, + fields: ["metadata", "product.*", "my_custom.*"], + filters: { + product_id: "prod_123", + }, +}) ``` -In this example, multiple users can’t have the same email. - - -# Add Data Model Check Constraints - -In this chapter, you'll learn how to add check constraints to your data model. - -## What is a Check Constraint? - -A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown. +This retrieves the product of id `prod_123` and its linked `my_custom` records. -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. +In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link. *** -## How to Set a Check Constraint? - -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 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. +## Update Custom Column's Value -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. +Link's `create` method updates a link's data if the link between the specified records already exists. -You can also pass an object to the `checks` method: +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. -```ts highlights={checks2Highlights} -import { model } from "@medusajs/framework/utils" +For example: -const CustomProduct = model.define("custom_product", { - // ... - price: model.bigNumber(), -}) -.checks([ - { - name: "custom_product_price_check", - expression: (columns) => `${columns.price} >= 0`, +```ts +await link.create({ + [Modules.PRODUCT]: { + product_id: "123", }, -]) -``` - -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). - -*** - -## Apply in Migrations - -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: - -```bash -npx medusa db:generate custom_module -npx medusa db:migrate + HELLO_MODULE: { + my_custom_id: "321", + }, + data: { + metadata: { + test: false, + }, + }, +}) ``` -The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. - # Emit Workflow and Service Events @@ -8034,6661 +7652,7043 @@ If you execute the `performAction` method of your service, the event is emitted Any subscribers listening to the event are also executed. -# Data Model Default Properties - -In this chapter, you'll learn about the properties available by default in your data model. - -When you create a data model, the following properties are created for you by Medusa: - -- `created_at`: A `dateTime` property that stores when a record of the data model was created. -- `updated_at`: A `dateTime` property that stores when a record of the data model was updated. -- `deleted_at`: A `dateTime` property that stores when a record of the data model was deleted. When you soft-delete a record, Medusa sets the `deleted_at` property to the current date. - +# Seed Data with Custom CLI Script -# Data Model Database Index +In this chapter, you'll learn how to seed data using a custom CLI script. -In this chapter, you’ll learn how to define a database index on a data model. +## How to Seed Data -## Define Database Index on Property +To seed dummy data for development or demo purposes, use a custom CLI script. -Use the `index` method on a property's definition to define a database index. +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. -For example: +### Example: Seed Dummy Products -```ts highlights={highlights} -import { model } from "@medusajs/framework/utils" +In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products. -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text().index( - "IDX_MY_CUSTOM_NAME" - ), -}) +First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script: -export default MyCustom +```bash npm2yarn +npm install --save-dev @faker-js/faker ``` -The `index` method optionally accepts the name of the index as a parameter. - -In this example, you define an index on the `name` property. - -*** +Then, create the file `src/scripts/demo-products.ts` with the following content: -## Define Database Index on Data Model - -A data model has an `indexes` method that defines database indices on its properties. +```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" -The index can be on multiple columns (composite index). For example: +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 + ) -```ts highlights={dataModelIndexHighlights} -import { model } from "@medusajs/framework/utils" + const defaultSalesChannel = await salesChannelModuleService + .listSalesChannels({ + name: "Default Sales Channel", + }) -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - }, -]) + const sizeOptions = ["S", "M", "L", "XL"] + const colorOptions = ["Black", "White"] + const currency_code = "eur" + const productsNum = 50 -export default MyCustom + // TODO seed products +} ``` -The `indexes` method receives an array of indices as a parameter. Each index is an object with a required `on` property indicating the properties to apply the index on. - -In the above example, you define a composite index on the `name` and `age` properties. - -### Index Conditions +So far, in the script, you: -An index can have conditions. 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 highlights={conditionHighlights} -import { model } from "@medusajs/framework/utils" +Next, replace the `TODO` with the following: -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - where: { - age: 30, - }, - }, -]) +```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 default MyCustom +// TODO seed products ``` -The index object passed to `indexes` accepts a `where` property whose value is an object of conditions. The object's key is a property's name, and its value is the condition on that property. - -In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`. - -A property's condition can be a negation. For example: +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. -```ts highlights={negationHighlights} -import { model } from "@medusajs/framework/utils" +Then, replace the new `TODO` with the following: -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, - }, - }, +```ts title="src/scripts/demo-products.ts" +const { result: products } = await createProductsWorkflow(container).run({ + input: { + products: productsData, }, -]) +}) -export default MyCustom +logger.info(`Seeded ${products.length} products.`) + +// TODO add inventory levels ``` -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. +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 the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`. +Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following: -### Unique Database Index +```ts title="src/scripts/demo-products.ts" +logger.info("Seeding inventory levels.") -The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index. +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={uniqueHighlights} -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(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - unique: true, +await createInventoryLevelsWorkflow(container).run({ + input: { + inventory_levels: inventoryLevels, }, -]) +}) -export default MyCustom +logger.info("Finished seeding inventory levels data.") ``` -This creates a unique composite index on the `name` and `age` properties. +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. -# Infer Type of Data Model +Finally, you use the `createInventoryLevelsWorkflow` from Medusa's core workflows to create the inventory levels. -In this chapter, you'll learn how to infer the type of a data model. +### Test Script -## How to Infer Type of Data Model? +To test out the script, run the following command in your project's directory: -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. +```bash +npx medusa exec ./src/scripts/demo-products.ts +``` -Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. +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. -For example: -```ts -import { InferTypeOf } from "@medusajs/framework/types" -import { MyCustom } from "../models/my-custom" // relative path to the model +# Link -export type MyCustom = InferTypeOf<typeof MyCustom> -``` +In this chapter, you’ll learn what Link is and how to use it to manage links. -`InferTypeOf` accepts as a type argument the type of the data model. +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. -Since the `MyCustom` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. +## What is Link? -You can now use the `MyCustom` type to reference a data model in other types, such as in workflow inputs or service method outputs: +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. -```ts title="Example Service" -// other imports... -import { InferTypeOf } from "@medusajs/framework/types" -import { MyCustom } from "../models/my-custom" +For example: -type MyCustom = InferTypeOf<typeof MyCustom> +```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -class HelloModuleService extends MedusaService({ MyCustom }) { - async doSomething(): Promise<MyCustom> { - // ... - } +export async function POST( + req: MedusaRequest, + res: MedusaResponse +): Promise<void> { + const link = req.scope.resolve( + ContainerRegistrationKeys.LINK + ) + + // ... } ``` +You can use its methods to manage links, such as create or delete links. -# Manage Relationships - -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 +## Create Link -### BelongsTo Side of One-to-One +To create a link between records of two data models, use the `create` method of Link. -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. +For example: -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: +```ts +import { Modules } from "@medusajs/framework/utils" -```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", +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + "helloModuleService": { + my_custom_id: "mc_123", + }, }) ``` -In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to. - -### HasOne Side - -When you create a record of a data model that has one of another, pass the ID of the other data model's record in the relation property. - -For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set a user's email ID as follows: +The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules. -```ts highlights={hasOneHighlights} -// when creating a user -const user = await helloModuleService.createUsers({ - // other properties... - email: "123", -}) +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. -// when updating a user -const user = await helloModuleService.updateUsers({ - id: "321", - // other properties... - email: "123", -}) -``` +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. -In the example above, you pass the `email` property when creating or updating a user to specify the email it has. +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. *** -## Manage One-to-Many Relationship +## Dismiss Link -In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. +To remove a link between records of two data models, use the `dismiss` method of Link. -When you create a record of the data model on the `belongsTo` side, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property. +For example: -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: +```ts +import { Modules } from "@medusajs/framework/utils" -```ts highlights={manyBelongsHighlights} -// when creating a product -const product = await helloModuleService.createProducts({ - // other properties... - store_id: "123", -}) +// ... -// when updating a product -const product = await helloModuleService.updateProducts({ - id: "321", - // other properties... - store_id: "123", +await link.dismiss({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + "helloModuleService": { + my_custom_id: "mc_123", + }, }) ``` -In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to. +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. *** -## Manage Many-to-Many Relationship +## Cascade Delete Linked Records -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. +If a record is deleted, use the `delete` method of Link to delete all linked records. -### Create Associations +For example: -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. +```ts +import { Modules } from "@medusajs/framework/utils" -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: +// ... -```ts highlights={manyHighlights} -// when creating a product -const product = await helloModuleService.createProducts({ - // other properties... - orders: ["123", "321"], -}) +await productModuleService.deleteVariants([variant.id]) -// when creating an order -const order = await helloModuleService.createOrders({ - id: "321", - // other properties... - products: ["123", "321"], +await link.delete({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, }) ``` -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. +This deletes all records linked to the deleted product. -### Update Associations +*** -When you use the `update` methods generated by the service factory, you also pass an array of IDs as the relation property's value to add new associated records. +## Restore Linked Records -However, this removes any existing associations to records whose IDs aren't included in the array. +If a record that was previously soft-deleted is now restored, use the `restore` method of Link to restore all linked records. -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: +For example: ```ts -const product = await helloModuleService.updateProducts({ - id: "123", - // other properties... - orders: ["321"], -}) -``` - -If the product was associated with an order, and you don't include that order's ID in the `orders` array, the association between the product and order is removed. +import { Modules } from "@medusajs/framework/utils" -So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product: +// ... -```ts highlights={updateAssociationHighlights} -const product = await helloModuleService.retrieveProduct( - "123", - { - relations: ["orders"], - } -) +await productModuleService.restoreProducts(["prod_123"]) -const updatedProduct = await helloModuleService.updateProducts({ - id: product.id, - // other properties... - orders: [ - ...product.orders.map((order) => order.id), - "321", - ], +await link.restore({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, }) ``` -This keeps existing associations between the product and orders, and adds a new one. -*** +# Query Context -## Manage Many-to-Many Relationship with pivotEntity +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 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. +## What is Query Context? -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. +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, 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: +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). -```ts highlights={["4"]} -class HelloModuleService extends MedusaService({ - Order, - Product, - OrderProduct, -}) {} -``` +*** -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. +## How to Use Query Context -For example: +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`). -```ts -// create order-product association -const orderProduct = await helloModuleService.createOrderProducts({ - order_id: "123", - product_id: "123", - metadata: { - test: true, - }, -}) +You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument. -// update order-product association -const orderProduct = await helloModuleService.updateOrderProducts({ - id: "123", - metadata: { - test: false, - }, -}) +For example, to retrieve posts using Query while passing the user's language as a context: -// delete order-product association -await helloModuleService.deleteOrderProducts("123") +```ts +const { data } = await query.graph({ + entity: "post", + fields: ["*"], + context: QueryContext({ + lang: "es", + }), +}) ``` -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`. +In this example, you pass in the context a `lang` property whose value is `es`. -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. +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. -*** +For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context: -## Retrieve Records of Relation +```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" -The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options. +class BlogModuleService extends MedusaService({ + Post, + Author, +}){ + // @ts-ignore + async listPosts( + filters?: any, + config?: FindConfig<any> | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context -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. + let posts = await super.listPosts(filters, config, sharedContext) -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: + if (context.lang === "es") { + posts = posts.map((post) => { + return { + ...post, + title: post.title + " en español", + } + }) + } -```ts highlights={retrieveHighlights} -const product = await helloModuleService.retrieveProducts( - "123", - { - relations: ["orders"], + return posts } -) +} + +export default BlogModuleService ``` -In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. +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. +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. -# Data Model’s Primary Key +All posts returned will now have their titles appended with "en español". -In this chapter, you’ll learn how to configure the primary key of a data model. +Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md). -## primaryKey Method +### Using Pagination with Query -To set any `id`, `text`, or `number` property as a primary key, use the `primaryKey` method. +If you pass pagination fields to `query.graph`, you must also override the `listAndCount` method in the service. -For example: +For example, following along with the previous example, you must override the `listAndCountPosts` method of the Blog Module's service: -```ts highlights={highlights} -import { model } from "@medusajs/framework/utils" +```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" -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - // ... -}) +class BlogModuleService extends MedusaService({ + Post, + Author, +}){ + // @ts-ignore + async listAndCountPosts( + filters?: any, + config?: FindConfig<any> | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context -export default MyCustom -``` + const result = await super.listAndCountPosts( + filters, + config, + sharedContext + ) -In the example above, the `id` property is defined as the data model's primary key. + if (context.lang === "es") { + result.posts = posts.map((post) => { + return { + ...post, + title: post.title + " en español", + } + }) + } + return result + } +} -# Data Model Property Types +export default BlogModuleService +``` -In this chapter, you’ll learn about the types of properties in a data model’s schema. +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`. -## id +*** -The `id` method defines an automatically generated string ID property. The generated ID is a unique string that has a mix of letters and numbers. +## Passing Query Context to Related Data Models -For example: +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. -```ts highlights={idHighlights} -import { model } from "@medusajs/framework/utils" +For linked data models, check out the [next section](#passing-query-context-to-linked-data-models). -const MyCustom = model.define("my_custom", { - id: model.id(), - // ... -}) +For example, to pass a context for the post's authors: -export default MyCustom +```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: -## text +```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" -The `text` method defines a string property. +class BlogModuleService extends MedusaService({ + Post, + Author, +}){ + // @ts-ignore + async listPosts( + filters?: any, + config?: FindConfig<any> | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context -For example: + let posts = await super.listPosts(filters, config, sharedContext) -```ts highlights={textHighlights} -import { model } from "@medusajs/framework/utils" + const isPostLangEs = context.lang === "es" + const isAuthorLangEs = context.author?.lang === "es" -const MyCustom = model.define("my_custom", { - name: model.text(), - // ... -}) + 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, + }, + } + }) + } -export default MyCustom + return posts + } +} + +export default BlogModuleService ``` -*** +The context in `filters` will also have the context for `author`, which you can use to make transformations to the post's authors. -## number +*** -The `number` method defines a number property. +## Passing Query Context to Linked Data Models -For example: +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. -```ts highlights={numberHighlights} -import { model } from "@medusajs/framework/utils" +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: -const MyCustom = model.define("my_custom", { - age: model.number(), - // ... +```ts highlights={highlights5} +const { data } = await query.graph({ + entity: "product", + fields: ["*", "post.*"], + context: { + post: QueryContext({ + lang: "es", + }), + }, }) - -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. -## float +To handle the context, you override the generated `listPosts` method of the Blog Module as explained [previously](#how-to-use-query-context). -This property is only available after [Medusa v2.1.2](https://github.com/medusajs/medusa/releases/tag/v2.1.2). -The `float` method defines a number property that allows for values with decimal places. +# Module Link Direction -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). +In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case. -For example: +## Link Direction -```ts highlights={floatHighlights} -import { model } from "@medusajs/framework/utils" +The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`. -const MyCustom = model.define("my_custom", { - rating: model.float(), - // ... -}) +For example, the following defines a link from the `helloModuleService`'s `myCustom` data model to the Product Module's `product` data model: -export default MyCustom +```ts +export default defineLink( + HelloModule.linkable.myCustom, + ProductModule.linkable.product +) ``` -*** +Whereas the following defines a link from the Product Module's `product` data model to the `helloModuleService`'s `myCustom` data model: -## bigNumber +```ts +export default defineLink( + ProductModule.linkable.product, + HelloModule.linkable.myCustom +) +``` -The `bigNumber` method defines a number property that expects large numbers, such as prices. +The above links are two different links that serve different purposes. -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). +*** -For example: +## Which Link Direction to Use? -```ts highlights={bigNumberHighlights} -import { model } from "@medusajs/framework/utils" +### Extend Data Models -const MyCustom = model.define("my_custom", { - price: model.bigNumber(), - // ... -}) +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. -export default MyCustom +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 +) ``` -*** +### Associate Data Models -## boolean +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. -The `boolean` method defines a boolean property. +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`: -For example: +```ts +export default defineLink( + HelloModule.linkable.post, + ProductModule.linkable.product +) +``` -```ts highlights={booleanHighlights} -import { model } from "@medusajs/framework/utils" -const MyCustom = model.define("my_custom", { - hasAccount: model.boolean(), - // ... -}) +# Query -export default MyCustom -``` +In this chapter, you’ll learn about Query and how to use it to fetch data from modules. -*** +## What is Query? -### enum +Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key. -The `enum` method defines a property whose value can only be one of the specified values. +In your resources, such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s commerce modules. -For example: +*** -```ts highlights={enumHighlights} -import { model } from "@medusajs/framework/utils" +## Query Example -const MyCustom = model.define("my_custom", { - color: model.enum(["black", "white"]), - // ... -}) +For example, create the route `src/api/query/route.ts` with the following content: -export default MyCustom -``` +```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" -The `enum` method accepts an array of possible string values. +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) -*** + const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + }) -## dateTime + res.json({ my_customs: myCustoms }) +} +``` -The `dateTime` method defines a timestamp property. +In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key. -For example: +Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties: -```ts highlights={dateTimeHighlights} -import { model } from "@medusajs/framework/utils" +- `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. -const MyCustom = model.define("my_custom", { - date_of_birth: model.dateTime(), - // ... -}) +The method returns an object that has a `data` property, which holds an array of the retrieved data. For example: -export default MyCustom +```json title="Returned Data" +{ + "data": [ + { + "id": "123", + "name": "test" + } + ] +} ``` *** -## json - -The `json` method defines a property whose value is a stringified JSON object. - -For example: - -```ts highlights={jsonHighlights} -import { model } from "@medusajs/framework/utils" +## Querying the Graph -const MyCustom = model.define("my_custom", { - metadata: model.json(), - // ... -}) +When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates. -export default MyCustom -``` +This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them. *** -## array +## Retrieve Linked Records -The `array` method defines an array of strings property. +Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`. For example: -```ts highlights={arrHightlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - names: model.array(), - // ... +```ts highlights={[["6"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: [ + "id", + "name", + "product.*", + ], }) - -export default MyCustom ``` -*** +`.*` 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`. -## Properties Reference +### Retrieve List Link Records -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. +If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`. +For example: -# Write Migration +```ts highlights={[["6"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: [ + "id", + "name", + "products.*", + ], +}) +``` -In this chapter, you'll learn how to create a migration and write it manually. +### Apply Filters and Pagination on Linked Records -## What is a Migration? +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. -A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It has two methods: +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. -- The `up` method reflects changes on the database. -- The `down` method reverts the changes made in the `up` method. +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: -## How to Write a Migration? +```ts highlights={queryLinkTableHighlights} +import productCustomLink from "../../../links/product-custom" -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. +// ... -Alternatively, you can manually create a migration file under the `migrations` directory of your module. +const { data: productCustoms } = await query.graph({ + entity: productCustomLink.entryPoint, + fields: ["*", "product.*", "my_custom.*"], + pagination: { + take: 5, + skip: 0, + }, +}) +``` -For example: +In the object passed to the `graph` method: -```ts title="src/modules/hello/migrations/Migration20240429.ts" -import { Migration } from "@mikro-orm/migrations" +- 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. -export class Migration20240702105919 extends Migration { +You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination). - async up(): Promise<void> { - this.addSql("create table if not exists \"my_custom\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"my_custom_pkey\" primary key (\"id\"));") - } +The returned `data` is similar to the following: - async down(): Promise<void> { - this.addSql("drop table if exists \"my_custom\" cascade;") +```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... } - -} +}] ``` -The migration's file name should be of the format `Migration{YEAR}{MONTH}{DAY}.ts`. The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. - -In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax. - -In the example above, the `up` method creates the table `my_custom`, and the `down` method drops the table if the migration is reverted. - -Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. - *** -## Run the Migration +## Apply Filters -To run your migration, run the following command: +```ts highlights={[["6"], ["7"], ["8"], ["9"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + filters: { + id: [ + "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX", + "mc_01HWSVWK3KYHKQEE6QGS2JC3FX", + ], + }, +}) +``` -This command also syncs module links. If you don't want that, use the `--skip-links` option. +The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records. -```bash -npx medusa db:migrate -``` +In the example above, you filter the `my_custom` records by multiple IDs. -This reflects the changes in the database as implemented in the migration's `up` method. +Filters don't apply on fields of linked data models from other modules. *** -## Rollback the Migration - -To rollback or revert the last migration you ran for a module, run the following command: +## Apply Pagination -```bash -npx medusa db:rollback helloModuleService +```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, + }, +}) ``` -This rolls back the last ran migration on the Hello Module. +The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records. -*** +To paginate the returned records, pass the following properties to `pagination`: -## More Database Commands +- `skip`: (required to apply pagination) The number of records to skip before fetching the results. +- `take`: The number of records to fetch. -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). +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: +- skip: (\`number\`) The number of records skipped. +- take: (\`number\`) The number of records requested to fetch. +- count: (\`number\`) The total number of records. -# Searchable Data Model Property +### Sort Records -In this chapter, you'll learn what a searchable property is and how to define it. +```ts highlights={[["5"], ["6"], ["7"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + pagination: { + order: { + name: "DESC", + }, + }, +}) +``` -## What is a Searchable Property? +Sorting doesn't work on fields of linked data models from other modules. -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. +To sort returned records, pass an `order` property to `pagination`. -When the `q` filter is passed, the data model's searchable properties are queried to find matching records. +The `order` property is an object whose keys are property names, and values are either: + +- `ASC` to sort records by that property in ascending order. +- `DESC` to sort records by that property in descending order. *** -## Define a Searchable Property +## Request Query Configurations -Use the `searchable` method on a `text` property to indicate that it's searchable. +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={searchableHighlights} -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", { - name: model.text().searchable(), - // ... -}) +### 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, the `name` property is searchable. - -### Search Example +```ts title="src/api/middlewares.ts" +import { + validateAndTransformQuery, + defineMiddlewares, +} from "@medusajs/framework/http" +import { createFindParams } from "@medusajs/medusa/api/utils/validators" -If you pass a `q` filter to the `listMyCustoms` method: +export const GetCustomSchema = createFindParams() -```ts -const myCustoms = await helloModuleService.listMyCustoms({ - q: "John", +export default defineMiddlewares({ + routes: [ + { + matcher: "/customs", + method: "GET", + middlewares: [ + validateAndTransformQuery( + GetCustomSchema, + { + defaults: [ + "id", + "name", + "products.*", + ], + isList: true, + } + ), + ], + }, + ], }) ``` -This retrieves records that include `John` in their `name` property. +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`. -# Data Model Relationships +### Step 2: Use Configurations in API Route -In this chapter, you’ll learn how to define relationships between data models in your module. +After applying this middleware, your API route now accepts the `fields`, `offset`, `limit`, and `order` query parameters mentioned above. -## What is a Relationship Property? +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. -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`. +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. -When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type. +For example, Create the file `src/api/customs/route.ts` with the following content: -You want to create a relation between data models in the same module. +```ts title="src/api/customs/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -You want to create a relationship between data models in different modules. Use module links instead. +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, + }) -## One-to-One Relationship + res.json({ my_customs: myCustoms }) +} +``` -A one-to-one relationship indicates that one record of a data model belongs to or is associated with another. +This adds a `GET` API route at `/customs`, which is the API route you added the middleware for. -To define a one-to-one relationship, create relationship properties in the data models using the following methods: +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. -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. +### Test it Out -For example: +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. -```ts highlights={oneToOneHighlights} -import { model } from "@medusajs/framework/utils" +```json title="Returned Data" +{ + "my_customs": [ + { + "id": "123", + "name": "test" + } + ] +} +``` -const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email), -}) +Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result. -const Email = model.define("email", { - id: model.id().primaryKey(), - user: model.belongsTo(() => User, { - mappedBy: "email", - }), -}) -``` +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. -In the example above, a user has one email, and an email belongs to one user. -The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model. +# Add Data Model Check Constraints -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. +In this chapter, you'll learn how to add check constraints to your data model. -### Optional Relationship +## What is a Check Constraint? -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). +A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown. -### One-sided One-to-One Relationship +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. -If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. +*** -For example: +## How to Set a Check Constraint? -```ts highlights={oneToOneUndefinedHighlights} +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 User = model.define("user", { - id: model.id().primaryKey(), +const CustomProduct = model.define("custom_product", { + // ... + price: model.bigNumber(), }) +.checks([ + (columns) => `${columns.price} >= 0`, +]) +``` -const Email = model.define("email", { - id: model.id().primaryKey(), - user: model.belongsTo(() => User, { - mappedBy: undefined, - }), +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. + +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: + +```ts highlights={checks2Highlights} +import { model } from "@medusajs/framework/utils" + +const CustomProduct = model.define("custom_product", { + // ... + price: model.bigNumber(), }) +.checks([ + { + name: "custom_product_price_check", + expression: (columns) => `${columns.price} >= 0`, + }, +]) ``` -### One-to-One Relationship in the Database +The object accepts the following properties: -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: +- `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). -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. +*** - +## Apply in Migrations -*** +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. -## One-to-Many Relationship +To generate a migration for the data model's module then reflect it on the database, run the following command: -A one-to-many relationship indicates that one record of a data model has many records of another data model. +```bash +npx medusa db:generate custom_module +npx medusa db:migrate +``` -To define a one-to-many relationship, create relationship properties in the data models using the following methods: +The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. -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. + +# Configure Data Model Properties + +In this chapter, you’ll learn how to configure data model properties. + +## Property’s Default Value + +Use the `default` method on a property's definition to specify the default value of a property. For example: -```ts highlights={oneToManyHighlights} +```ts highlights={defaultHighlights} import { model } from "@medusajs/framework/utils" -const Store = model.define("store", { - id: model.id().primaryKey(), - products: model.hasMany(() => Product), +const MyCustom = model.define("my_custom", { + color: model + .enum(["black", "white"]) + .default("black"), + age: model + .number() + .default(0), + // ... }) -const Product = model.define("product", { - id: model.id().primaryKey(), - store: model.belongsTo(() => Store, { - mappedBy: "products", - }), -}) +export default MyCustom ``` -In this example, a store has many products, but a product belongs to one store. +In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`. -### 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). +## Nullable Property -### One-to-Many Relationship in the Database +Use the `nullable` method to indicate that a property’s value can be `null`. -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: +For example: -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. +```ts highlights={nullableHighlights} +import { model } from "@medusajs/framework/utils" - +const MyCustom = model.define("my_custom", { + price: model.bigNumber().nullable(), + // ... +}) -*** +export default MyCustom +``` -## Many-to-Many Relationship +*** -A many-to-many relationship indicates that many records of a data model can be associated with many records of another data model. +## Unique Property -To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method. +The `unique` method indicates that a property’s value must be unique in the database through a unique index. For example: -```ts highlights={manyToManyHighlights} +```ts highlights={uniqueHighlights} 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 User = model.define("user", { + email: model.text().unique(), + // ... }) -const Product = model.define("product", { - id: model.id().primaryKey(), - orders: model.manyToMany(() => Order, { - mappedBy: "products", - }), -}) +export default User ``` -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. +In this example, multiple users can’t have the same email. -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. +# Data Model Default Properties -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. +In this chapter, you'll learn about the properties available by default in your data model. -### Many-to-Many Relationship in the Database +When you create a data model, the following properties are created for you by Medusa: -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`. +- `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. -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. -The pivot table has columns with foreign keys pointing to the primary key of the associated tables. The column's name is either: +# Infer Type of Data Model -- 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`. +In this chapter, you'll learn how to infer the type of a data model. - +## How to Infer Type of Data Model? -### Many-To-Many with Custom Columns +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. -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. +Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. For example: -```ts highlights={manyToManyColumnHighlights} -import { model } from "@medusajs/framework/utils" +```ts +import { InferTypeOf } from "@medusajs/framework/types" +import { MyCustom } from "../models/my-custom" // relative path to the model -export const Order = model.define("order_test", { - id: model.id().primaryKey(), - products: model.manyToMany(() => Product, { - pivotEntity: () => OrderProduct, - }), -}) +export type MyCustom = InferTypeOf<typeof MyCustom> +``` -export const Product = model.define("product_test", { - id: model.id().primaryKey(), - orders: model.manyToMany(() => Order), -}) +`InferTypeOf` accepts as a type argument the type of the data model. -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(), -}) -``` +Since the `MyCustom` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. -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. +You can now use the `MyCustom` type to reference a data model in other types, such as in workflow inputs or service method outputs: -The `OrderProduct` model defines, aside from the ID, the following properties: +```ts title="Example Service" +// other imports... +import { InferTypeOf } from "@medusajs/framework/types" +import { MyCustom } from "../models/my-custom" -- `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. +type MyCustom = InferTypeOf<typeof MyCustom> -*** +class HelloModuleService extends MedusaService({ MyCustom }) { + async doSomething(): Promise<MyCustom> { + // ... + } +} +``` -## 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. +# Data Model Database Index -This is useful if the relationship property’s name is different from that of the associated data model. +In this chapter, you’ll learn how to define a database index on a data model. -As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. +## Define Database Index on Property + +Use the `index` method on a property's definition to define a database index. For example: -```ts highlights={relationNameHighlights} +```ts highlights={highlights} import { model } from "@medusajs/framework/utils" -const User = model.define("user", { +const MyCustom = model.define("my_custom", { id: model.id().primaryKey(), - email: model.hasOne(() => Email, { - mappedBy: "owner", - }), + name: model.text().index( + "IDX_MY_CUSTOM_NAME" + ), }) -const Email = model.define("email", { - id: model.id().primaryKey(), - owner: model.belongsTo(() => User, { - mappedBy: "email", - }), -}) +export default MyCustom ``` -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`. - -*** +The `index` method optionally accepts the name of the index as a parameter. -## Cascades +In this example, you define an index on the `name` property. -When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it. +*** -For example, if a store is deleted, its products should also be deleted. +## Define Database Index on Data Model -The `cascades` method used on a data model configures which child records an operation is cascaded to. +A data model has an `indexes` method that defines database indices on its properties. -For example: +The index can be on multiple columns (composite index). For example: -```ts highlights={highlights} +```ts highlights={dataModelIndexHighlights} import { model } from "@medusajs/framework/utils" -const Store = model.define("store", { +const MyCustom = model.define("my_custom", { id: model.id().primaryKey(), - products: model.hasMany(() => Product), -}) -.cascades({ - delete: ["products"], -}) + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + }, +]) -const Product = model.define("product", { - id: model.id().primaryKey(), - store: model.belongsTo(() => Store, { - mappedBy: "products", - }), -}) +export default MyCustom ``` -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. +The `indexes` method receives an array of indices as a parameter. Each index is an object with a required `on` property indicating the properties to apply the index on. -In the example above, when a store is deleted, its associated products are also deleted. +In the above example, you define a composite index on the `name` and `age` properties. +### Index Conditions -# Create a Plugin +An index can have conditions. For example: -In this chapter, you'll learn how to create a Medusa plugin and publish it. +```ts highlights={conditionHighlights} +import { model } from "@medusajs/framework/utils" -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. +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + where: { + age: 30, + }, + }, +]) -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). +export default MyCustom +``` -## 1. Create a Plugin Project +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. -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. +In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`. -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: +A property's condition can be a negation. For example: -```bash -npx create-medusa-app my-plugin --plugin +```ts highlights={negationHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number().nullable(), +}).indexes([ + { + on: ["name", "age"], + where: { + age: { + $ne: null, + }, + }, + }, +]) + +export default MyCustom ``` -This will create a new Medusa plugin project in the `my-plugin` directory. +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. -### Plugin Directory Structure +In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`. -After the installation is done, the plugin structure will look like this: +### Unique Database Index - +The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index. -- `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: -*** +```ts highlights={uniqueHighlights} +import { model } from "@medusajs/framework/utils" -## 2. Prepare Plugin +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + unique: true, + }, +]) -### Package Name +export default MyCustom +``` -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. +This creates a unique composite index on the `name` and `age` properties. -For example: -```json title="package.json" -{ - "name": "@myorg/plugin-name", - // ... -} -``` +# Manage Relationships -### Package Keywords +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. -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: +## Manage One-to-One Relationship -```json title="package.json" -{ - "keywords": [ - "medusa-plugin", - "medusa-v2" - ], - // ... -} +### 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. + +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: + +```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", +}) ``` -### Package Dependencies +In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to. -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. +### HasOne Side -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. +When you create a record of a data model that has one of another, pass the ID of the other data model's record in the relation property. -For example, assuming `2.5.0` is the latest Medusa version: +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: -```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", - } -} -``` +```ts highlights={hasOneHighlights} +// when creating a user +const user = await helloModuleService.createUsers({ + // other properties... + email: "123", +}) -*** +// when updating a user +const user = await helloModuleService.updateUsers({ + id: "321", + // other properties... + email: "123", +}) +``` -## 3. Publish Plugin Locally for Development and Testing +In the example above, you pass the `email` property when creating or updating a user to specify the email it has. -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. +*** -### Publish and Install Local Package +## Manage One-to-Many Relationship -### Prerequisites +In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. -- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) +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. -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. +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: -To publish the plugin to the local registry, run the following command in your plugin project: +```ts highlights={manyBelongsHighlights} +// when creating a product +const product = await helloModuleService.createProducts({ + // other properties... + store_id: "123", +}) -```bash title="Plugin project" -npx medusa plugin:publish +// when updating a product +const product = await helloModuleService.updateProducts({ + id: "321", + // other properties... + store_id: "123", +}) ``` -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`. +In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to. -Next, navigate to your Medusa application: +*** -```bash title="Medusa application" -cd ~/path/to/medusa-app -``` +## Manage Many-to-Many Relationship -Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. +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. -Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: +### Create Associations -```bash npm2yarn title="Medusa application" -npm install --save-dev yalc -``` +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. -After that, run the following Medusa CLI command to install the plugin: +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: -```bash title="Medusa application" -npx medusa plugin:add @myorg/plugin-name +```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"], +}) ``` -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. +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. -### Register Plugin in Medusa Application +### Update Associations -After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. +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. -Add the plugin to the `plugins` array in the `medusa-config.ts` file: +However, this removes any existing associations to records whose IDs aren't included in the array. -```ts title="medusa-config.ts" highlights={pluginHighlights} -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "@myorg/plugin-name", - options: {}, - }, - ], +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 +const product = await helloModuleService.updateProducts({ + id: "123", + // other properties... + orders: ["321"], }) ``` -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 +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. -Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. +So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product: -For example: +```ts highlights={updateAssociationHighlights} +const product = await helloModuleService.retrieveProduct( + "123", + { + relations: ["orders"], + } +) -```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "@myorg/plugin-name", - options: { - apiKey: true, - }, - }, +const updatedProduct = await helloModuleService.updateProducts({ + id: product.id, + // other properties... + orders: [ + ...product.orders.map((order) => order.id), + "321", ], }) ``` -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). +This keeps existing associations between the product and orders, and adds a new one. -### Watch Plugin Changes During Development +*** -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. +## Manage Many-to-Many Relationship with pivotEntity -To do that, run the following command in your plugin project: +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. -```bash title="Plugin project" -npx medusa plugin:develop +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. + +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: + +```ts highlights={["4"]} +class HelloModuleService extends MedusaService({ + Order, + Product, + OrderProduct, +}) {} ``` -This command will: +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. -- 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. +For example: -### Start Medusa Application +```ts +// create order-product association +const orderProduct = await helloModuleService.createOrderProducts({ + order_id: "123", + product_id: "123", + metadata: { + test: true, + }, +}) -You can start your Medusa application's development server to test out your plugin: +// update order-product association +const orderProduct = await helloModuleService.updateOrderProducts({ + id: "123", + metadata: { + test: false, + }, +}) -```bash npm2yarn title="Medusa application" -npm run dev +// delete order-product association +await helloModuleService.deleteOrderProducts("123") ``` -While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. +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. *** -## 4. Create Customizations in the Plugin +## Retrieve Records of Relation -You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. +The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options. -- [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) +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. -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). +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: -### Generating Migrations for Modules +```ts highlights={retrieveHighlights} +const product = await helloModuleService.retrieveProducts( + "123", + { + relations: ["orders"], + } +) +``` -During your development, you may need to generate migrations for modules in your plugin. To do that, use the `plugin:db:generate` command: +In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. -```bash title="Plugin project" -npx medusa plugin:db:generate -``` -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: +# Data Model’s Primary Key -```bash title="Medusa application" -npx medusa db:migrate -``` +In this chapter, you’ll learn how to configure the primary key of a data model. -### Importing Module Resources +## primaryKey Method -Your plugin project should have the following exports in `package.json`: +To set any `id`, `text`, or `number` property as a primary key, use the `primaryKey` method. -```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" - } -} +For example: + +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + // ... +}) + +export default MyCustom ``` -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 the example above, the `id` property is defined as the data model's primary key. -The plugin exports the following files and directories: -- `./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. +# Data Model Property Types -With these exports, you can import the plugin's resources in the Medusa application's code like this: +In this chapter, you’ll learn about the types of properties in a data model’s schema. -`@myorg/plugin-name` is the plugin package's name. +## id -```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" -``` +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. -And you can register a module provider in the Medusa application's `medusa-config.ts` like this: +For example: -```ts highlights={[["9"]]} title="medusa-config.ts" -module.exports = defineConfig({ +```ts highlights={idHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + id: model.id(), // ... - 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. +export default MyCustom +``` -### Create Module Providers +*** -To learn how to create module providers, refer to the following guides: +## text -- [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 `text` method defines a string property. -*** +For example: -## 5. Publish Plugin to NPM +```ts highlights={textHighlights} +import { model } from "@medusajs/framework/utils" -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: +const MyCustom = model.define("my_custom", { + name: model.text(), + // ... +}) -```bash -npx medusa plugin:build +export default MyCustom ``` -The command will compile an output in the `.medusa/server` directory. +*** -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: +## number -```bash -npm publish -``` +The `number` method defines a number property. -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. +For example: -### Install Public Plugin in Medusa Application +```ts highlights={numberHighlights} +import { model } from "@medusajs/framework/utils" -You install a plugin that's published publicly using your package manager. For example: +const MyCustom = model.define("my_custom", { + age: model.number(), + // ... +}) -```bash npm2yarn -npm install @myorg/plugin-name +export default MyCustom ``` -Where `@myorg/plugin-name` is the name of your plugin as published on NPM. - -Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). - *** -## Update a Published Plugin +## float -To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md). +This property is only available after [Medusa v2.1.2](https://github.com/medusajs/medusa/releases/tag/v2.1.2). -If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. +The `float` method defines a number property that allows for values with decimal places. -First, run the following command to change the version of the plugin: +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). -```bash -npm version <type> -``` +For example: -Where `<type>` 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 highlights={floatHighlights} +import { model } from "@medusajs/framework/utils" -Then, re-run the same commands for publishing a plugin: +const MyCustom = model.define("my_custom", { + rating: model.float(), + // ... +}) -```bash -npx medusa plugin:build -npm publish +export default MyCustom ``` -This will publish an updated version of your plugin under a new version. - +*** -# Add Columns to a Link +## bigNumber -In this chapter, you'll learn how to add custom columns to a link definition and manage them. +The `bigNumber` method defines a number property that expects large numbers, such as prices. -## How to Add Custom Columns to a Link's Table? +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). -The `defineLink` function used to define a link accepts a third parameter, which is an object of options. +For example: -To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property: +```ts highlights={bigNumberHighlights} +import { model } from "@medusajs/framework/utils" -```ts highlights={linkHighlights} -import HelloModule from "../modules/hello" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" +const MyCustom = model.define("my_custom", { + price: model.bigNumber(), + // ... +}) -export default defineLink( - ProductModule.linkable.product, - HelloModule.linkable.myCustom, - { - database: { - extraColumns: { - metadata: { - type: "json", - }, - }, - }, - } -) +export default MyCustom ``` -This adds to the table created for the link between `product` and `myCustom` a `metadata` column of type `json`. - -### Database Options +*** -The `database` property defines configuration for the table created in the database. +## boolean -Its `extraColumns` property defines custom columns to create in the link's table. +The `boolean` method defines a boolean property. -`extraColumns`'s value is an object whose keys are the names of the columns, and values are the column's configurations as an object. +For example: -### Column Configurations +```ts highlights={booleanHighlights} +import { model } from "@medusajs/framework/utils" -The column's configurations object accepts the following properties: +const MyCustom = model.define("my_custom", { + hasAccount: model.boolean(), + // ... +}) -- `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. +export default MyCustom +``` *** -## Set Custom Column when Creating Link +### enum -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. +The `enum` method defines a property whose value can only be one of the specified values. 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 highlights={enumHighlights} +import { model } from "@medusajs/framework/utils" -```ts -await link.create({ - [Modules.PRODUCT]: { - product_id: "123", - }, - HELLO_MODULE: { - my_custom_id: "321", - }, - data: { - metadata: { - test: true, - }, - }, +const MyCustom = model.define("my_custom", { + color: model.enum(["black", "white"]), + // ... }) + +export default MyCustom ``` +The `enum` method accepts an array of possible string values. + *** -## Retrieve Custom Column with Link +## dateTime -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. +The `dateTime` method defines a timestamp property. For example: -```ts highlights={retrieveHighlights} -import productHelloLink from "../links/product-hello" - -// ... +```ts highlights={dateTimeHighlights} +import { model } from "@medusajs/framework/utils" -const { data } = await query.graph({ - entity: productHelloLink.entryPoint, - fields: ["metadata", "product.*", "my_custom.*"], - filters: { - product_id: "prod_123", - }, +const MyCustom = model.define("my_custom", { + date_of_birth: model.dateTime(), + // ... }) -``` -This retrieves the product of id `prod_123` and its linked `my_custom` records. - -In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link. +export default MyCustom +``` *** -## Update Custom Column's Value - -Link's `create` method updates a link's data if the link between the specified records already exists. +## json -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. +The `json` method defines a property whose value is a stringified JSON object. For example: -```ts -await link.create({ - [Modules.PRODUCT]: { - product_id: "123", - }, - HELLO_MODULE: { - my_custom_id: "321", - }, - data: { - metadata: { - test: false, - }, - }, +```ts highlights={jsonHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + metadata: model.json(), + // ... }) + +export default MyCustom ``` +*** -# Module Link Direction +## array -In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case. +The `array` method defines an array of strings property. -## Link Direction +For example: -The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`. +```ts highlights={arrHightlights} +import { model } from "@medusajs/framework/utils" -For example, the following defines a link from the `helloModuleService`'s `myCustom` data model to the Product Module's `product` data model: +const MyCustom = model.define("my_custom", { + names: model.array(), + // ... +}) -```ts -export default defineLink( - HelloModule.linkable.myCustom, - ProductModule.linkable.product -) +export default MyCustom ``` -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 -) -``` +## Properties Reference -The above links are two different links that serve different purposes. +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. + + +# Searchable Data Model Property + +In this chapter, you'll learn what a searchable property is and how to define it. + +## What is a Searchable Property? + +Methods generated by the [service factory](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. *** -## Which Link Direction to Use? +## Define a Searchable Property -### Extend Data Models +Use the `searchable` method on a `text` property to indicate that it's searchable. -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: -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 highlights={searchableHighlights} +import { model } from "@medusajs/framework/utils" -```ts -export default defineLink( - ProductModule.linkable.product, - HelloModule.linkable.subtitle -) +const MyCustom = model.define("my_custom", { + name: model.text().searchable(), + // ... +}) + +export default MyCustom ``` -### Associate Data Models +In this example, the `name` property is searchable. -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. +### Search Example -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`: +If you pass a `q` filter to the `listMyCustoms` method: ```ts -export default defineLink( - HelloModule.linkable.post, - ProductModule.linkable.product -) +const myCustoms = await helloModuleService.listMyCustoms({ + q: "John", +}) ``` +This retrieves records that include `John` in their `name` property. -# Link - -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. +# Data Model Relationships -## What is Link? +In this chapter, you’ll learn how to define relationships between data models in your module. -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. +## What is a Relationship Property? -For example: +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`. -```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type. -export async function POST( - req: MedusaRequest, - res: MedusaResponse -): Promise<void> { - const link = req.scope.resolve( - ContainerRegistrationKeys.LINK - ) - - // ... -} -``` +You want to create a relation between data models in the same module. -You can use its methods to manage links, such as create or delete links. +You want to create a relationship between data models in different modules. Use module links instead. *** -## Create Link +## One-to-One Relationship -To create a link between records of two data models, use the `create` method of Link. +A one-to-one relationship indicates that one record of a data model belongs to or is associated with another. + +To define a one-to-one relationship, create relationship properties in the data models using the following methods: + +1. `hasOne`: indicates that the model has one record of the specified model. +2. `belongsTo`: indicates that the model belongs to one record of the specified model. For example: -```ts -import { Modules } from "@medusajs/framework/utils" +```ts highlights={oneToOneHighlights} +import { model } from "@medusajs/framework/utils" -// ... +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email), +}) -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_123", - }, +const Email = model.define("email", { + id: model.id().primaryKey(), + user: model.belongsTo(() => User, { + mappedBy: "email", + }), }) ``` -The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules. +In the example above, a user has one email, and an email belongs to one user. -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 `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model. -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. +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. -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. +### 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). -## Dismiss Link +### One-sided One-to-One Relationship -To remove a link between records of two data models, use the `dismiss` method of Link. +If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. For example: -```ts -import { Modules } from "@medusajs/framework/utils" +```ts highlights={oneToOneUndefinedHighlights} +import { model } from "@medusajs/framework/utils" -// ... +const User = model.define("user", { + id: model.id().primaryKey(), +}) -await link.dismiss({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_123", - }, +const Email = model.define("email", { + id: model.id().primaryKey(), + user: model.belongsTo(() => User, { + mappedBy: undefined, + }), }) ``` -The `dismiss` method accepts the same parameter type as the [create method](#create-link). +### One-to-One Relationship in the Database -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. +When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property: + +1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column. +2. A foreign key on the `{relation_name}_id` column to the table of the related data model. + + *** -## Cascade Delete Linked Records +## One-to-Many Relationship -If a record is deleted, use the `delete` method of Link to delete all linked records. +A one-to-many relationship indicates that one record of a data model has many records of another data model. -For example: +To define a one-to-many relationship, create relationship properties in the data models using the following methods: -```ts -import { Modules } from "@medusajs/framework/utils" +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. -// ... +For example: -await productModuleService.deleteVariants([variant.id]) +```ts highlights={oneToManyHighlights} +import { model } from "@medusajs/framework/utils" -await link.delete({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, +const Store = model.define("store", { + id: model.id().primaryKey(), + products: model.hasMany(() => Product), +}) + +const Product = model.define("product", { + id: model.id().primaryKey(), + store: model.belongsTo(() => Store, { + mappedBy: "products", + }), }) ``` -This deletes all records linked to the deleted product. +In this example, a store has many products, but a product belongs to one store. + +### Optional Relationship + +To make the relationship optional on the `belongsTo` side, use the `nullable` method on the property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/configure-properties#nullable-property/index.html.md). + +### One-to-Many Relationship in the Database + +When you generate the migrations of data models that have a one-to-many relationship, the migration adds to the table of the data model that has the `belongsTo` property: + +1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `product` table will have a `store_id` column. +2. A foreign key on the `{relation_name}_id` column to the table of the related data model. + + *** -## Restore Linked Records +## Many-to-Many Relationship -If a record that was previously soft-deleted is now restored, use the `restore` method of Link to restore all linked records. +A many-to-many relationship indicates that many records of a data model can be associated with many records of another data model. -For example: +To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method. -```ts -import { Modules } from "@medusajs/framework/utils" +For example: -// ... +```ts highlights={manyToManyHighlights} +import { model } from "@medusajs/framework/utils" -await productModuleService.restoreProducts(["prod_123"]) +const Order = model.define("order", { + id: model.id().primaryKey(), + products: model.manyToMany(() => Product, { + mappedBy: "orders", + pivotTable: "order_product", + joinColumn: "order_id", + inverseJoinColumn: "product_id", + }), +}) -await link.restore({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, +const Product = model.define("product", { + id: model.id().primaryKey(), + orders: model.manyToMany(() => Order, { + mappedBy: "products", + }), }) ``` +The `manyToMany` method accepts two parameters: -# Query +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. -In this chapter, you’ll learn about Query and how to use it to fetch data from modules. +The `pivotTable`, `joinColumn`, and `inverseJoinColumn` properties are only available after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). -## What is Query? +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. -Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key. +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. -In your resources, such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s commerce modules. +### Many-to-Many Relationship in the Database -*** +When you generate the migrations of data models that have a many-to-many relationship, the migration adds a new pivot table. 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`. -## Query Example +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, create the route `src/api/query/route.ts` with the following content: +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/query/route.ts" highlights={exampleHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +- 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 const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + - const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], - }) +### Many-To-Many with Custom Columns - res.json({ my_customs: myCustoms }) -} -``` +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. -In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key. +For example: -Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties: +```ts highlights={manyToManyColumnHighlights} +import { model } from "@medusajs/framework/utils" -- `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. +export const Order = model.define("order_test", { + id: model.id().primaryKey(), + products: model.manyToMany(() => Product, { + pivotEntity: () => OrderProduct, + }), +}) -The method returns an object that has a `data` property, which holds an array of the retrieved data. For example: +export const Product = model.define("product_test", { + id: model.id().primaryKey(), + orders: model.manyToMany(() => Order), +}) -```json title="Returned Data" -{ - "data": [ - { - "id": "123", - "name": "test" - } - ] -} +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(), +}) ``` -*** - -## Querying the Graph +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. -When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates. +The `OrderProduct` model defines, aside from the ID, the following properties: -This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them. +- `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. *** -## Retrieve Linked Records - -Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`. +## 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 highlights={[["6"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: [ - "id", - "name", - "product.*", - ], -}) -``` +This is useful if the relationship property’s name is different from that of the associated data model. -`.*` 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`. +As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. -### 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 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 highlights={[["6"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: [ - "id", - "name", - "products.*", - ], +const Email = model.define("email", { + id: model.id().primaryKey(), + owner: model.belongsTo(() => User, { + mappedBy: "email", + }), }) ``` -### Apply Filters and Pagination on Linked Records +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`. -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. +*** -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. +## Cascades -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. +When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it. + +For example, if a store is deleted, its products should also be deleted. + +The `cascades` method used on a data model configures which child records an operation is cascaded to. For example: -```ts highlights={queryLinkTableHighlights} -import productCustomLink from "../../../links/product-custom" +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" -// ... +const Store = model.define("store", { + id: model.id().primaryKey(), + products: model.hasMany(() => Product), +}) +.cascades({ + delete: ["products"], +}) -const { data: productCustoms } = await query.graph({ - entity: productCustomLink.entryPoint, - fields: ["*", "product.*", "my_custom.*"], - pagination: { - take: 5, - skip: 0, - }, +const Product = model.define("product", { + id: model.id().primaryKey(), + store: model.belongsTo(() => Store, { + mappedBy: "products", + }), }) ``` -In the object passed to the `graph` method: +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. -- 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 the example above, when a store is deleted, its associated products are also deleted. -You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination). -The returned `data` is similar to the following: +# Write Migration -```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... - } -}] -``` - -*** - -## Apply Filters - -```ts highlights={[["6"], ["7"], ["8"], ["9"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], - filters: { - id: [ - "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX", - "mc_01HWSVWK3KYHKQEE6QGS2JC3FX", - ], - }, -}) -``` +In this chapter, you'll learn how to create a migration and write it manually. -The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records. +## What is a Migration? -In the example above, you filter the `my_custom` records by multiple IDs. +A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It has two methods: -Filters don't apply on fields of linked data models from other modules. +- The `up` method reflects changes on the database. +- The `down` method reverts the changes made in the `up` method. *** -## Apply Pagination +## How to Write a Migration? -```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, - }, -}) -``` +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 `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records. +Alternatively, you can manually create a migration file under the `migrations` directory of your module. -To paginate the returned records, pass the following properties to `pagination`: +For example: -- `skip`: (required to apply pagination) The number of records to skip before fetching the results. -- `take`: The number of records to fetch. +```ts title="src/modules/hello/migrations/Migration20240429.ts" +import { Migration } from "@mikro-orm/migrations" -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: +export class Migration20240702105919 extends Migration { -- skip: (\`number\`) The number of records skipped. -- take: (\`number\`) The number of records requested to fetch. -- count: (\`number\`) The total number of records. + async up(): Promise<void> { + this.addSql("create table if not exists \"my_custom\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"my_custom_pkey\" primary key (\"id\"));") + } -### Sort Records + async down(): Promise<void> { + this.addSql("drop table if exists \"my_custom\" cascade;") + } -```ts highlights={[["5"], ["6"], ["7"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], - pagination: { - order: { - name: "DESC", - }, - }, -}) +} ``` -Sorting doesn't work on fields of linked data models from other modules. +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`. -To sort returned records, pass an `order` property to `pagination`. +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. -The `order` property is an object whose keys are property names, and values are either: +In the example above, the `up` method creates the table `my_custom`, and the `down` method drops the table if the migration is reverted. -- `ASC` to sort records by that property in ascending order. -- `DESC` to sort records by that property in descending order. +Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. *** -## Request Query Configurations +## Run the Migration -For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that: +To run your migration, run the following command: -- 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. +This command also syncs module links. If you don't want that, use the `--skip-links` option. -Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request. +```bash +npx medusa db:migrate +``` -### Step 1: Add Middleware +This reflects the changes in the database as implemented in the migration's `up` method. -The first step is to use the `validateAndTransformQuery` middleware on the `GET` route. You add the middleware in `src/api/middlewares.ts`: +*** -```ts title="src/api/middlewares.ts" -import { - validateAndTransformQuery, - defineMiddlewares, -} from "@medusajs/framework/http" -import { createFindParams } from "@medusajs/medusa/api/utils/validators" +## Rollback the Migration -export const GetCustomSchema = createFindParams() +To rollback or revert the last migration you ran for a module, run the following command: -export default defineMiddlewares({ - routes: [ - { - matcher: "/customs", - method: "GET", - middlewares: [ - validateAndTransformQuery( - GetCustomSchema, - { - defaults: [ - "id", - "name", - "products.*", - ], - isList: true, - } - ), - ], - }, - ], -}) +```bash +npx medusa db:rollback helloModuleService ``` -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`. - -### Step 2: Use Configurations in API Route - -After applying this middleware, your API route now accepts the `fields`, `offset`, `limit`, and `order` query parameters mentioned above. +This rolls back the last ran migration on the Hello Module. -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. +*** -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. +## More Database Commands -For example, Create the file `src/api/customs/route.ts` with the following content: +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). -```ts title="src/api/customs/route.ts" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) +# Create a Plugin - const { data: myCustoms } = await query.graph({ - entity: "my_custom", - ...req.queryConfig, - }) +In this chapter, you'll learn how to create a Medusa plugin and publish it. - res.json({ my_customs: myCustoms }) -} -``` +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. -This adds a `GET` API route at `/customs`, which is the API route you added the middleware for. +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). -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. +## 1. Create a Plugin Project -### Test it Out +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. -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. +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: -```json title="Returned Data" -{ - "my_customs": [ - { - "id": "123", - "name": "test" - } - ] -} +```bash +npx create-medusa-app my-plugin --plugin ``` -Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result. - -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. - - -# Query Context +This will create a new Medusa plugin project in the `my-plugin` directory. -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). +### Plugin Directory Structure -## What is Query Context? +After the installation is done, the plugin structure will look like this: -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). +- `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. *** -## How to Use Query Context +## 2. Prepare Plugin -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`). +### Package Name -You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument. +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, to retrieve posts using Query while passing the user's language as a context: +For example: -```ts -const { data } = await query.graph({ - entity: "post", - fields: ["*"], - context: QueryContext({ - lang: "es", - }), -}) +```json title="package.json" +{ + "name": "@myorg/plugin-name", + // ... +} ``` -In this example, you pass in the context a `lang` property whose value is `es`. +### Package Keywords -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. +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: -For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context: +```json title="package.json" +{ + "keywords": [ + "medusa-plugin", + "medusa-v2" + ], + // ... +} +``` -```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" +### Package Dependencies -class BlogModuleService extends MedusaService({ - Post, - Author, -}){ - // @ts-ignore - async listPosts( - filters?: any, - config?: FindConfig<any> | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context +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. - let posts = await super.listPosts(filters, config, sharedContext) +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. - if (context.lang === "es") { - posts = posts.map((post) => { - return { - ...post, - title: post.title + " en español", - } - }) - } +For example, assuming `2.5.0` is the latest Medusa version: - return posts +```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 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. - -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. +*** -All posts returned will now have their titles appended with "en español". +## 3. Publish Plugin Locally for Development and Testing -Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md). +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. -### Using Pagination with Query +### Publish and Install Local Package -If you pass pagination fields to `query.graph`, you must also override the `listAndCount` method in the service. +### Prerequisites -For example, following along with the previous example, you must override the `listAndCountPosts` method of the Blog Module's service: +- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) -```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" +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. -class BlogModuleService extends MedusaService({ - Post, - Author, -}){ - // @ts-ignore - async listAndCountPosts( - filters?: any, - config?: FindConfig<any> | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context +To publish the plugin to the local registry, run the following command in your plugin project: - const result = await super.listAndCountPosts( - filters, - config, - sharedContext - ) +```bash title="Plugin project" +npx medusa plugin:publish +``` - if (context.lang === "es") { - result.posts = posts.map((post) => { - return { - ...post, - title: post.title + " en español", - } - }) - } +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`. - return result - } -} +Next, navigate to your Medusa application: -export default BlogModuleService +```bash title="Medusa application" +cd ~/path/to/medusa-app ``` -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`. +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: -## Passing Query Context to Related Data Models +```bash npm2yarn title="Medusa application" +npm install --save-dev yalc +``` -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. +After that, run the following Medusa CLI command to install the plugin: -For linked data models, check out the [next section](#passing-query-context-to-linked-data-models). +```bash title="Medusa application" +npx medusa plugin:add @myorg/plugin-name +``` -For example, to pass a context for the post's authors: +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. -```ts highlights={highlights3} -const { data } = await query.graph({ - entity: "post", - fields: ["*"], - context: QueryContext({ - lang: "es", - author: QueryContext({ - lang: "es", - }), - }), -}) -``` +### Register Plugin in Medusa Application -Then, in the `listPosts` method, you can handle the context for the post's authors: +After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. -```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" +Add the plugin to the `plugins` array in the `medusa-config.ts` file: -class BlogModuleService extends MedusaService({ - Post, - Author, -}){ - // @ts-ignore - async listPosts( - filters?: any, - config?: FindConfig<any> | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context +```ts title="medusa-config.ts" highlights={pluginHighlights} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: {}, + }, + ], +}) +``` - let posts = await super.listPosts(filters, config, sharedContext) +The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package. - const isPostLangEs = context.lang === "es" - const isAuthorLangEs = context.author?.lang === "es" +#### Pass Module Options through Plugin - 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, - }, - } - }) - } +Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. - return posts - } -} +For example: -export default BlogModuleService +```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: { + apiKey: true, + }, + }, + ], +}) ``` -The context in `filters` will also have the context for `author`, which you can use to make transformations to the post's authors. - -*** +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). -## Passing Query Context to Linked Data Models +### Watch Plugin Changes During Development -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. +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. -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: +To do that, run the following command in your plugin project: -```ts highlights={highlights5} -const { data } = await query.graph({ - entity: "product", - fields: ["*", "post.*"], - context: { - post: QueryContext({ - lang: "es", - }), - }, -}) +```bash title="Plugin project" +npx medusa plugin:develop ``` -In this example, you retrieve products and their associated posts. You also pass a context for `post`, indicating the customer's language. +This command will: -To handle the context, you override the generated `listPosts` method of the Blog Module as explained [previously](#how-to-use-query-context). +- 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. +### Start Medusa Application -# Architectural Modules +You can start your Medusa application's development server to test out your plugin: -In this chapter, you’ll learn about architectural modules. +```bash npm2yarn title="Medusa application" +npm run dev +``` -## What is an Architectural Module? +While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. -An architectural module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. +*** -Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis. +## 4. Create Customizations in the Plugin -*** +You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. -## Architectural Module Types +- [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) -There are different architectural module types including: +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). - +### Generating Migrations for Modules -- 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. +During your development, you may need to generate migrations for modules in your plugin. To do that, use the `plugin:db:generate` command: -*** +```bash title="Plugin project" +npx medusa plugin:db:generate +``` -## Architectural Modules List +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: -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. +```bash title="Medusa application" +npx medusa db:migrate +``` +### Importing Module Resources -# Commerce Modules +Your plugin project should have the following exports in `package.json`: -In this chapter, you'll learn about Medusa's commerce modules. +```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" + } +} +``` -## What is a Commerce Module? +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. -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. +The plugin exports the following files and directories: -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. +- `./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. -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) +With these exports, you can import the plugin's resources in the Medusa application's code like this: -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. +`@myorg/plugin-name` is the plugin package's name. -### List of Medusa's Commerce Modules +```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" +``` -Refer to [this reference](https://docs.medusajs.com/resources/commerce-modules/index.html.md) for a full list of commerce modules in Medusa. +And you can register a module provider in the Medusa application's `medusa-config.ts` like this: -*** +```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... + }, + }, + ], + }, + }, + ], +}) +``` -## Use Commerce Modules in Custom Flows +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. -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. +### Create Module Providers -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: +To learn how to create module providers, refer to the following guides: -```ts highlights={highlights} -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +- [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) -export const countProductsStep = createStep( - "count-products", - async ({ }, { container }) => { - const productModuleService = container.resolve("product") +*** - const [,count] = await productModuleService.listAndCountProducts() +## 5. Publish Plugin to NPM - return new StepResponse(count) - } -) +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: + +```bash +npx medusa plugin:build ``` -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. +The command will compile an output in the `.medusa/server` directory. +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: -# Module Container +```bash +npm publish +``` -In this chapter, you'll learn about the module's container and how to resolve resources in that container. +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. -Since modules are isolated, each module has a local container only used by the resources of that module. +### Install Public Plugin in Medusa Application -So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container. +You install a plugin that's published publicly using your package manager. For example: -### List of Registered Resources +```bash npm2yarn +npm install @myorg/plugin-name +``` -Find a list of resources or dependencies registered in a module's container in [this Development Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). +Where `@myorg/plugin-name` is the name of your plugin as published on NPM. + +Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). *** -## Resolve Resources +## Update a Published Plugin -### Services +To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md). -A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. +If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. -For example: +First, run the following command to change the version of the plugin: -```ts highlights={[["4"], ["10"]]} -import { Logger } from "@medusajs/framework/types" +```bash +npm version <type> +``` -type InjectedDependencies = { - logger: Logger -} +Where `<type>` 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. -export default class HelloModuleService { - protected logger_: Logger +Then, re-run the same commands for publishing a plugin: - constructor({ logger }: InjectedDependencies) { - this.logger_ = logger +```bash +npx medusa plugin:build +npm publish +``` - this.logger_.info("[HelloModuleService]: Hello World!") - } +This will publish an updated version of your plugin under a new version. - // ... -} -``` -### Loader +# Access Workflow Errors -A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources. +In this chapter, you’ll learn how to access errors that occur during a workflow’s execution. + +## How to Access Workflow Errors? + +By default, when an error occurs in a workflow, it throws that error, and the execution stops. + +You can configure the workflow to return the errors instead so that you can access and handle them differently. For example: -```ts highlights={[["9"]]} -import { - LoaderOptions, -} from "@medusajs/framework/types" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import myWorkflow from "../../../workflows/hello-world" -export default async function helloWorldLoader({ - container, -}: LoaderOptions) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result, errors } = await myWorkflow(req.scope) + .run({ + // ... + throwOnError: false, + }) - logger.info("[helloWorldLoader]: Hello, World!") + if (errors.length) { + return res.send({ + errors: errors.map((error) => error.error), + }) + } + + res.send(result) } + ``` +The object passed to the `run` method accepts a `throwOnError` property. When disabled, the errors are returned in the `errors` property of `run`'s output. -# Perform Database Operations in a Service +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. -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. +# Scheduled Jobs Number of Executions -## Run Queries +In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. -[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. +## numberOfExecutions Option -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). +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. -So, to run database queries in a service: +For example: -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` +```ts highlights={highlights} +export default async function myCustomJob() { + console.log("I'll be executed three times only.") +} -For example, in your service, add the following methods: +export const config = { + name: "hello-world", + // execute every minute + schedule: "* * * * *", + numberOfExecutions: 3, +} +``` -```ts highlights={methodsHighlight} -// other imports... -import { - InjectManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { SqlEntityManager } from "@mikro-orm/knex" +The above scheduled job has the `numberOfExecutions` configuration set to `3`. -class HelloModuleService { - // ... +So, it'll only execute 3 times, each every minute, then it won't be executed anymore. - @InjectManager() - async getCount( - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<number> { - return await sharedContext.manager.count("my_custom") - } - - @InjectManager() - async getCountSql( - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<number> { - const data = await sharedContext.manager.execute( - "SELECT COUNT(*) as num FROM my_custom" - ) - - return parseInt(data[0].num) - } -} -``` +If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. -You add two methods `getCount` and `getCountSql` that have the `InjectManager` decorator. Each of the methods also accept the `sharedContext` parameter which has the `MedusaContext` decorator. -The entity manager is injected to the `sharedContext.manager` property, which is an instance of [EntityManager from the @mikro-orm/knex package](https://mikro-orm.io/api/knex/class/EntityManager). +# Expose a Workflow Hook -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. +In this chapter, you'll learn how to expose a hook in your workflow. -Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. +## When to Expose a Hook -*** +Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow. -## Execute Operations in Transactions +Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead. -To wrap database operations in a transaction, you create two methods: +*** -1. A private or protected method that's wrapped in a transaction. To wrap it in a transaction, you use the `InjectTransactionManager` decorator 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. +## How to Expose a Hook in a Workflow? -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. +To expose a hook in your workflow, use `createHook` from the Workflows SDK. For example: -```ts highlights={opHighlights} -import { - InjectManager, - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" - -class HelloModuleService { - // ... - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<any> { - const transactionManager = sharedContext.transactionManager - await transactionManager.nativeUpdate( - "my_custom", - { - id: input.id, - }, - { - name: input.name, - } - ) +```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" - // retrieve again - const updatedRecord = await transactionManager.execute( - `SELECT * FROM my_custom WHERE id = '${input.id}'` +export const myWorkflow = createWorkflow( + "my-workflow", + function (input) { + const product = createProductStep(input) + const productCreatedHook = createHook( + "productCreated", + { productId: product.id } ) - return updatedRecord - } - - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ) { - return await this.update_(input, sharedContext) + return new WorkflowResponse(product, { + hooks: [productCreatedHook], + }) } -} +) ``` -The `HelloModuleService` has two methods: - -- A protected `update_` that performs the database operations inside a transaction. -- A public `update` that executes the transactional protected method. +The `createHook` function accepts two parameters: -The shared context's `transactionManager` property holds the transactional entity manager (injected by `InjectTransactionManager`) that you use to perform database operations. +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. -Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. +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. -### Why Wrap a Transactional Method +### How to Consume the Hook? -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. +To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content: -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. +```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights} +import { myWorkflow } from "../my-workflow" -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. +myWorkflow.hooks.productCreated( + async ({ productId }, { container }) => { + // TODO perform an action + } +) +``` -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. +The hook is available on the workflow's `hooks` property using its name `productCreated`. -For example, the `update` method could be changed to the following: +You invoke the hook, passing a step function (the hook handler) as a parameter. -```ts -// other imports... -import { EntityManager } from "@mikro-orm/knex" -class HelloModuleService { - // ... - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ) { - const newData = await this.update_(input, sharedContext) +# Workflow Constraints - await sendNewDataToSystem(newData) +This chapter lists constraints of defining a workflow or its steps. - return newData - } -} -``` +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 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. +This creates restrictions related to variable manipulations, using if-conditions, and other constraints. This chapter lists these constraints and provides their alternatives. -### Using Methods in Transactional Methods +## Workflow Constraints -If your transactional method uses other methods that accept a Medusa context, pass the shared context to those methods. +### No Async Functions -For example: +The function passed to `createWorkflow` can’t be an async function: -```ts -// other imports... -import { EntityManager } from "@mikro-orm/knex" +```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) { + // ... +}) -class HelloModuleService { +// Do +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { // ... - @InjectTransactionManager() - protected async anotherMethod( - @MedusaContext() sharedContext?: Context<EntityManager> - ) { - // ... - } - - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<any> { - this.anotherMethod(sharedContext) - } -} +}) ``` -You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context. +### No Direct Variable Manipulation -The `anotherMethod` now runs in the same transaction as the `update_` method. +You can’t directly manipulate variables within the workflow's constructor function. -*** +Learn more about why you can't manipulate variables [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) -## Configure Transactions +Instead, use `transform` from the Workflows SDK: -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. +```ts highlights={highlights} +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const str1 = step1(input) + const str2 = step2(input) -The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more. + return new WorkflowResponse({ + message: `${str1}${str2}`, + }) +}) -The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction. +// Do +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const str1 = step1(input) + const str2 = step2(input) -For example, resolve the `baseRepository` in your service's constructor: + const result = transform( + { + str1, + str2, + }, + (input) => ({ + message: `${input.str1}${input.str2}`, + }) + ) -### Extending Service Factory + return new WorkflowResponse(result) +}) +``` -```ts highlights={[["14"]]} -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" -import { DAL } from "@medusajs/framework/types" +### Create Dates in transform -type InjectedDependencies = { - baseRepository: DAL.RepositoryService -} +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. -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - protected baseRepository_: DAL.RepositoryService +Instead, create the date using `transform`. - constructor({ baseRepository }: InjectedDependencies) { - super(...arguments) - this.baseRepository_ = baseRepository - } -} - -export default HelloModuleService -``` - -### Without Service Factory +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). -```ts highlights={[["10"]]} -import { DAL } from "@medusajs/framework/types" +For example: -type InjectedDependencies = { - baseRepository: DAL.RepositoryService -} +```ts highlights={dateHighlights} +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const today = new Date() -class HelloModuleService { - protected baseRepository_: DAL.RepositoryService + return new WorkflowResponse({ + today, + }) +}) - constructor({ baseRepository }: InjectedDependencies) { - this.baseRepository_ = baseRepository - } -} +// Do +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const today = transform({}, () => new Date()) -export default HelloModuleService + return new WorkflowResponse({ + today, + }) +}) ``` -Then, add the following method that uses it: +### No If Conditions -```ts highlights={repoHighlights} -// ... -import { - InjectManager, - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" +You can't use if-conditions in a workflow. -class HelloModuleService { - // ... - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<any> { - return await this.baseRepository_.transaction( - async (transactionManager) => { - await transactionManager.nativeUpdate( - "my_custom", - { - id: input.id, - }, - { - name: input.name, - } - ) +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) - // retrieve again - const updatedRecord = await transactionManager.execute( - `SELECT * FROM my_custom WHERE id = '${input.id}'` - ) +Instead, use when-then from the Workflows SDK: - return updatedRecord - }, - { - transaction: sharedContext.transactionManager, - } - ) +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + if (input.is_active) { + // perform an action } +}) - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ) { - return await this.update_(input, sharedContext) - } -} +// 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 + }) +}) ``` -The `update_` method uses the `baseRepository_.transaction` method to wrap a function in a transaction. +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). -The function parameter receives a transactional entity manager as a parameter. Use it to perform the database operations. +### No Conditional Operators -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. +You can't use conditional operators in a workflow, such as `??` or `||`. -Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. +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) -### Transaction Options +Instead, use `transform` to store the desired value in a variable. -The second parameter of the `baseRepository_.transaction` method is an object of options that accepts the following properties: +### Logical Or (||) Alternative -1. `transaction`: Set the transactional entity manager passed to the function. You must provide this option as explained in the previous section. +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const message = input.message || "Hello" +}) -```ts highlights={[["16"]]} +// Do // other imports... -import { EntityManager } from "@mikro-orm/knex" +import { transform } from "@medusajs/framework/workflows-sdk" -class HelloModuleService { - // ... - @InjectTransactionManager() - async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<any> { - return await this.baseRepository_.transaction<EntityManager>( - async (transactionManager) => { - // ... - }, +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const message = transform( { - transaction: sharedContext.transactionManager, - } + input, + }, + (data) => data.input.message || "hello" ) - } -} +}) ``` -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` +### Nullish Coalescing (??) Alternative -```ts highlights={[["19"]]} +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const message = input.message ?? "Hello" +}) + +// Do // other imports... -import { IsolationLevel } from "@mikro-orm/core" +import { transform } from "@medusajs/framework/workflows-sdk" -class HelloModuleService { - // ... - @InjectTransactionManager() - async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<any> { - return await this.baseRepository_.transaction<EntityManager>( - async (transactionManager) => { - // ... - }, +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const message = transform( { - isolationLevel: IsolationLevel.READ_COMMITTED, - } - ) - } -} -``` - -3. `enableNestedTransactions`: (default: `false`) whether to allow using nested transactions. - - If `transaction` is provided and this is disabled, the manager in `transaction` is re-used. - -```ts highlights={[["16"]]} -class HelloModuleService { - // ... - @InjectTransactionManager() - async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context<EntityManager> - ): Promise<any> { - return await this.baseRepository_.transaction<EntityManager>( - async (transactionManager) => { - // ... + input, }, - { - enableNestedTransactions: false, - } + (data) => data.input.message ?? "hello" ) - } -} +}) ``` +### Double Not (!!) Alternative -# Module Isolation - -In this chapter, you'll learn how modules are isolated, and what that means for your custom development. - -- 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. +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + step1({ + isActive: !!input.is_active, + }) +}) -## How are Modules Isolated? +// Do +// other imports... +import { transform } from "@medusajs/framework/workflows-sdk" -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 myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const isActive = transform( + { + input, + }, + (data) => !!data.input.is_active + ) + + step1({ + isActive, + }) +}) +``` -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. +### Ternary Alternative -*** +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + step1({ + message: input.is_active ? "active" : "inactive", + }) +}) -## Why are Modules Isolated +// Do +// other imports... +import { transform } from "@medusajs/framework/workflows-sdk" -Some of the module isolation's benefits include: +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const message = transform( + { + input, + }, + (data) => { + return data.input.is_active ? "active" : "inactive" + } + ) + + step1({ + message, + }) +}) +``` -- 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. +### Optional Chaining (?.) Alternative -*** +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + step1({ + name: input.customer?.name, + }) +}) -## How to Extend Data Model of Another Module? +// Do +// other imports... +import { transform } from "@medusajs/framework/workflows-sdk" -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). +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const name = transform( + { + input, + }, + (data) => data.input.customer?.name + ) + + step1({ + name, + }) +}) +``` *** -## How to Use Services of Other Modules? - -If you're building a feature that uses functionalities from different modules, use a workflow whose steps resolve the modules' services to perform these functionalities. - -Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. +## Step Constraints -### Example +### Returned Values -For example, consider you have two modules: +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. -1. A module that stores and manages brands in your application. -2. A module that integrates a third-party Content Management System (CMS). +Values of other types, such as Maps, aren't allowed. -To sync brands from your application to the third-party system, create the following steps: +```ts +// Don't +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" -```ts title="Example Steps" highlights={stepsHighlights} -const retrieveBrandsStep = createStep( - "retrieve-brands", - async (_, { container }) => { - const brandModuleService = container.resolve( - "brandModuleService" - ) +const step1 = createStep( + "step-1", + (input, { container }) => { + const myMap = new Map() - const brands = await brandModuleService.listBrands() + // ... - return new StepResponse(brands) + return new StepResponse({ + myMap, + }) } ) -const createBrandsInCmsStep = createStep( - "create-brands-in-cms", - async ({ brands }, { container }) => { - const cmsModuleService = container.resolve( - "cmsModuleService" - ) +// Do +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" - const cmsBrands = await cmsModuleService.createBrands(brands) +const step1 = createStep( + "step-1", + (input, { container }) => { + const myObj: Record<string, unknown> = {} - return new StepResponse(cmsBrands, cmsBrands) - }, - async (brands, { container }) => { - const cmsModuleService = container.resolve( - "cmsModuleService" - ) + // ... - await cmsModuleService.deleteBrands( - brands.map((brand) => brand.id) - ) + return new StepResponse({ + myObj, + }) } ) ``` -The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module. - -Then, create the following workflow that uses these steps: - -```ts title="Example Workflow" -export const syncBrandsWorkflow = createWorkflow( - "sync-brands", - () => { - const brands = retrieveBrandsStep() - - createBrandsInCmsStep({ brands }) - } -) -``` -You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. +# Conditions in Workflows with When-Then +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. -# Modules Directory Structure +## Why If-Conditions Aren't Allowed in Workflows? -In this document, you'll learn about the expected files and directories in your module. +Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. At that point, variables in the workflow don't have any values. They only do when you execute the workflow. - +So, you can't use an if-condition that checks a variable's value, as the condition will be evaluated when Medusa creates the internal representation of the workflow, rather than during execution. -## index.ts +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. -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). +Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code. *** -## service.ts +## How to use When-Then? -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). +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: -## Other Directories +```ts highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + when, +} from "@medusajs/framework/workflows-sdk" +// step imports... -The following directories are optional and their content are explained more in the following chapters: +const workflow = createWorkflow( + "workflow", + function (input: { + is_active: boolean + }) { -- `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. + const result = when( + input, + (input) => { + return input.is_active + } + ).then(() => { + const stepResult = isActiveStep() + return stepResult + }) + // executed without condition + const anotherStepResult = anotherStep(result) -# Loaders + return new WorkflowResponse( + anotherStepResult + ) + } +) +``` -In this chapter, you’ll learn about loaders and how to use them. +In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`. -## What is a Loader? +### When Parameters -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. +`when` accepts the following parameters: -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. +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`. -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. +### Then Parameters -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). +To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. -*** +The callback function is only executed if `when`'s second parameter function returns a `true` value. -## How to Create a Loader? +*** -### 1. Implement Loader Function +## Implementing If-Else with When-Then -You create a loader function in a TypeScript or JavaScript file under a module's `loaders` directory. +when-then doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow. -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: +For example: - +```ts highlights={ifElseHighlights} +const workflow = createWorkflow( + "workflow", + function (input: { + is_active: boolean + }) { -Learn how to create a module in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + const isActiveResult = when( + input, + (input) => { + return input.is_active + } + ).then(() => { + return isActiveStep() + }) -```ts title="src/modules/hello/loaders/hello-world.ts" -import { - LoaderOptions, -} from "@medusajs/framework/types" - -export default async function helloWorldLoader({ - container, -}: LoaderOptions) { - const logger = container.resolve("logger") + const notIsActiveResult = when( + input, + (input) => { + return !input.is_active + } + ).then(() => { + return notIsActiveStep() + }) - logger.info("[helloWorldLoader]: Hello, World!") -} + // ... + } +) ``` -The loader file exports an async function, which is the function executed when the application loads. +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. -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. +*** -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). +## Specify Name for When-Then -### 2. Export Loader in Module Definition +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: -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. +```ts +const isActiveResult = when( + input, + (input) => { + return input.is_active + } +).then(() => { + return isActiveStep() +}) +``` -So, to export the loader you implemented above in the `hello` module, add the following to `src/modules/hello/index.ts`: +This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name. -```ts title="src/modules/hello/index.ts" -// other imports... -import helloWorldLoader from "./loaders/hello-world" +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. -export default Module("hello", { - // ... - loaders: [helloWorldLoader], +You pass a name for `when-then` as a first parameter of `when`, whose signature can accept three parameters in this case. For example: + +```ts highlights={nameHighlights} +const { isActive } = when( + "check-is-active", + input, + (input) => { + return input.is_active + } +).then(() => { + const isActive = isActiveStep() + + return { + isActive, + } }) ``` -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 +Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters: -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: +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`. -```bash npm2yarn -npm run dev -``` +The second and third parameters are the same as the parameters you previously passed to `when`. -Then, you'll find the following message logged in the terminal: -```plain -info: [HELLO MODULE] Just started the Medusa application! -``` +# Compensation Function -This indicates that the loader in the `hello` module ran and logged this message. +In this chapter, you'll learn what a compensation function is and how to add it to a step. -*** +## What is a Compensation Function -## Example: Register Custom MongoDB Connection +A compensation function rolls back or undoes changes made by a step when an error occurs in the workflow. -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. +For example, if a step creates a record, the compensation function deletes the record when an error occurs later in the workflow. -Consider your have a MongoDB module that allows you to perform operations on a MongoDB database. +By using compensation functions, you provide a mechanism that guarantees data consistency in your application and across systems. -### Prerequisites +*** -- [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) +## How to add a Compensation Function? -To connect to the database, you create the following loader in your module: +A compensation function is passed as a second parameter to the `createStep` function. -```ts title="src/modules/mongo/loaders/connection.ts" highlights={loaderHighlights} -import { LoaderOptions } from "@medusajs/framework/types" -import { asValue } from "awilix" -import { MongoClient } from "mongodb" +For example, create the file `src/workflows/hello-world.ts` with the following content: -type ModuleOptions = { - connection_url?: string - db_name?: string -} +```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" -export default async function mongoConnectionLoader({ - container, - options, -}: LoaderOptions<ModuleOptions>) { - 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) +const step1 = createStep( + "step-1", + async () => { + const message = `Hello from step one!` - logger.info("Connected to MongoDB") + console.log(message) - container.register( - "mongoClient", - asValue(clientDb) - ) - } catch (e) { - logger.error( - `[MONGO MDOULE]: An error occurred while connecting to MongoDB: ${e}` - ) + return new StepResponse(message) + }, + async () => { + console.log("Oops! Rolling back my changes...") } -} +) ``` -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: +Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow. -```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: +## Test the Compensation Function -- `connection_url`: the URL to connect to the MongoDB database. -- `db_name`: The name of the database to connect to. +Create a step in the same `src/workflows/hello-world.ts` file that throws an error: -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. +```ts title="src/workflows/hello-world.ts" +const step2 = createStep( + "step-2", + async () => { + throw new Error("Throwing an error...") + } +) +``` -After creating the client, you register it in the module's container using the container's `register` method. The method accepts two parameters: +Then, create a workflow that uses the steps: -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. +```ts title="src/workflows/hello-world.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +// other imports... -### Use Custom Registered Resource in Module's Service +// steps... -After registering the custom MongoDB client in the module's container, you can now resolve and use it in the module's service. +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str1 = step1() + step2() -For example: + return new WorkflowResponse({ + message: str1, + }) +}) -```ts title="src/modules/mongo/service.ts" -import type { Db } from "mongodb" +export default myWorkflow +``` -type InjectedDependencies = { - mongoClient: Db -} +Finally, execute the workflow from an API route: -export default class MongoModuleService { - private mongoClient_: Db +```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" - constructor({ mongoClient }: InjectedDependencies) { - this.mongoClient_ = mongoClient - } +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await myWorkflow(req.scope) + .run() - async createMovie({ title }: { - title: string - }) { - const moviesCol = this.mongoClient_.collection("movie") + res.send(result) +} +``` - const insertedMovie = await moviesCol.insertOne({ - title, - }) +Run the Medusa application and send a `GET` request to `/workflow`: - const movie = await moviesCol.findOne({ - _id: insertedMovie.insertedId, - }) +```bash +curl http://localhost:9000/workflow +``` - return movie - } +In the console, you'll see: - async deleteMovie(id: string) { - const moviesCol = this.mongoClient_.collection("movie") +- `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. - await moviesCol.deleteOne({ - _id: { - equals: id, - }, - }) - } -} -``` +*** -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. +## Pass Input to Compensation Function -Make sure to export the loader in the module's definition in the `index.ts` file at the root directory of the module: +If a step creates a record, the compensation function must receive the ID of the record to remove it. -```ts title="src/modules/mongo/index.ts" highlights={[["9"]]} -import { Module } from "@medusajs/framework/utils" -import MongoModuleService from "./service" -import mongoConnectionLoader from "./loaders/connection" +To pass input to the compensation function, pass a second parameter in the `StepResponse` returned by the step. -export const MONGO_MODULE = "mongo" +For example: -export default Module(MONGO_MODULE, { - service: MongoModuleService, - loaders: [mongoConnectionLoader], -}) +```ts highlights={inputHighlights} +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + return new StepResponse( + `Hello from step one!`, + { message: "Oops! Rolling back my changes..." } + ) + }, + async ({ message }) => { + console.log(message) + } +) ``` -### Test it Out +In this example, the step passes an object as a second parameter to `StepResponse`. -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: +The compensation function receives the object and uses its `message` property to log a message. -```bash -info: Connected to MongoDB -``` +*** -You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database. +## Resolve Resources from the Medusa Container +The compensation function receives an object second parameter. The object has a `container` property that you use to resolve resources from the Medusa container. -# Multiple Services in a Module +For example: -In this chapter, you'll learn how to use multiple services in a module. +```ts +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -## Module's Main and Internal Services +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 + ) -A module has one main service only, which is the service exported in the module's definition. + logger.info(message) + } +) +``` -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. +In this example, you use the `container` property in the second object parameter of the compensation function to resolve the logger. + +You then use the logger to log a message. *** -## How to Add an Internal Service +## Handle Errors in Loops -### 1. Create Service +This feature is only available after [Medusa v2.0.5](https://github.com/medusajs/medusa/releases/tag/v2.0.5). -To add an internal service, create it in the `services` directory of your module. +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: -For example, create the file `src/modules/hello/services/client.ts` with the following content: +```ts +// other imports... +import { promiseAll } from "@medusajs/framework/utils" -```ts title="src/modules/hello/services/client.ts" -export class ClientService { - async getMessage(): Promise<string> { - return "Hello, World!" - } +type StepInput = { + ids: string[] } -``` -### 2. Export Service in Index +const step1 = createStep( + "step-1", + async ({ ids }: StepInput, { container }) => { + const erpModuleService = container.resolve( + ERP_MODULE + ) + const prevData: unknown[] = [] -Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. + await promiseAll( + ids.map(async (id) => { + const data = await erpModuleService.retrieve(id) -For example, create the file `src/modules/hello/services/index.ts` with the following content: + await erpModuleService.delete(id) -```ts title="src/modules/hello/services/index.ts" -export * from "./client" + prevData.push(id) + }) + ) + + return new StepResponse(ids, prevData) + } +) ``` -This exports the `ClientService`. +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. -### 3. Resolve Internal Service +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. -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. +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: -For example, in your main service: +```ts highlights={highlights} +try { + await promiseAll( + ids.map(async (id) => { + const data = await erpModuleService.retrieve(id) -```ts title="src/modules/hello/service.ts" highlights={[["5"], ["13"]]} -// other imports... -import { ClientService } from "./services" + await erpModuleService.delete(id) -type InjectedDependencies = { - clientService: ClientService + prevData.push(id) + }) + ) +} catch (e) { + return StepResponse.permanentFailure( + `An error occurred: ${e}`, + prevData + ) } +``` -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - protected clientService_: ClientService +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. - constructor({ clientService }: InjectedDependencies) { - super(...arguments) - this.clientService_ = clientService - } -} -``` +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. -You can now use your internal service in your main service. -*** +# Execute Another Workflow -## Resolve Resources in Internal Service +In this chapter, you'll learn how to execute a workflow in another. -Resolve dependencies from your module's container in the constructor of your internal service. +## Execute in a Workflow -For example: +To execute a workflow in another, use the `runAsStep` method that every workflow has. -```ts -import { Logger } from "@medusajs/framework/types" +For example: -type InjectedDependencies = { - logger: Logger -} +```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports" +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" -export class ClientService { - protected logger_: Logger +const workflow = createWorkflow( + "hello-world", + async (input) => { + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + // ... + ], + }, + }) - constructor({ logger }: InjectedDependencies) { - this.logger_ = logger + // ... } -} +) ``` +Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. + +The object has an `input` property to pass input to the workflow. + *** -## Access Module Options +## Preparing Input Data -Your internal service can't access the module's options. +If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. -To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`. +Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). For example: -```ts -import { ConfigModule } from "@medusajs/framework/types" -import { HELLO_MODULE } from ".." +```ts highlights={transformHighlights} collapsibleLines="1-12" +import { + createWorkflow, + transform, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" -export type InjectedDependencies = { - configModule: ConfigModule +type WorkflowInput = { + title: string } -export class ClientService { - protected options: Record<string, any> +const workflow = createWorkflow( + "hello-product", + async (input: WorkflowInput) => { + const createProductsData = transform({ + input, + }, (data) => [ + { + title: `Hello ${data.input.title}`, + }, + ]) - constructor({ configModule }: InjectedDependencies) { - const moduleDef = configModule.modules[HELLO_MODULE] + const products = createProductsWorkflow.runAsStep({ + input: { + products: createProductsData, + }, + }) - if (typeof moduleDef !== "boolean") { - this.options = moduleDef.options - } + // ... } -} +) ``` -The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. +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`. -If its value is not a `boolean`, set the service's options to the module configuration's `options` property. +*** +## Run Workflow Conditionally -# Service Factory +To run a workflow in another based on a condition, use when-then from the Workflows SDK. -In this chapter, you’ll learn about what the service factory is and how to use it. +Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). -## What is the Service Factory? +For example: -Medusa provides a service factory that your module’s main service can extend. +```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" -The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually. +type WorkflowInput = { + product?: CreateProductWorkflowInputDTO + should_create?: boolean +} -Your service provides data-management functionalities of your data models. +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. -## How to Extend the Service Factory? -Medusa provides the service factory as a `MedusaService` function your service extends. The function creates and returns a service class with generated data-management methods. +# Run Workflow Steps in Parallel -For example, create the file `src/modules/hello/service.ts` with the following content: +In this chapter, you’ll learn how to run workflow steps in parallel. -```ts title="src/modules/hello/service.ts" highlights={highlights} -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" +## parallelize Utility Function -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - // TODO implement custom methods -} +If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK. -export default HelloModuleService -``` +The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step. -### MedusaService Parameters +For example: -The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for. +```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" -In the example above, since the `HelloModuleService` extends `MedusaService`, it has methods to manage the `MyCustom` data model, such as `createMyCustoms`. +interface WorkflowInput { + title: string +} -### Generated Methods +const myWorkflow = createWorkflow( + "my-workflow", + (input: WorkflowInput) => { + const product = createProductStep(input) -The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database. + const [prices, productSalesChannel] = parallelize( + createPricesStep(product), + attachProductToSalesChannelStep(product) + ) -The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`. + const id = product.id + const refetchedProduct = getProductStep(product.id) -For example, the following methods are generated for the service above: + return new WorkflowResponse(refetchedProduct) + } +) +``` -Find a complete reference of each of the methods in [this documentation](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) +The `parallelize` function accepts the steps to run in parallel as a parameter. -### listMyCustoms +It returns an array of the steps' results in the same order they're passed to the `parallelize` function. -### listMyCustoms +So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`. -This method retrieves an array of records based on filters and pagination configurations. -For example: +# Multiple Step Usage in Workflow -```ts -const myCustoms = await helloModuleService - .listMyCustoms() +In this chapter, you'll learn how to use a step multiple times in a workflow. -// with filters -const myCustoms = await helloModuleService - .listMyCustoms({ - id: ["123"] - }) -``` +## Problem Reusing a Step in a Workflow -### listAndCount +In some cases, you may need to use a step multiple times in the same workflow. -### retrieveMyCustom +The most common example is using the `useQueryGraphStep` multiple times in a workflow to retrieve multiple unrelated data, such as customers and products. -This method retrieves a record by its ID. +Each workflow step must have a unique ID, which is the ID passed as a first parameter when creating the step: -For example: +```ts +const useQueryGraphStep = createStep( + "use-query-graph" + // ... +) +``` + +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: ```ts -const myCustom = await helloModuleService - .retrieveMyCustom("123") +const helloWorkflow = createWorkflow( + "hello", + () => { + const { data: products } = useQueryGraphStep({ + entity: "product", + fields: ["id"], + }) + + // ERROR OCCURS HERE: A STEP HAS THE SAME ID AS ANOTHER IN THE WORKFLOW + const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: ["id"], + }) + } +) ``` -### retrieveMyCustom +The next section explains how to fix this issue to use the same step multiple times in a workflow. -### updateMyCustoms +*** -This method updates and retrieves records of the data model. +## How to Use a Step Multiple Times in a Workflow? -For example: +When you execute a step in a workflow, you can chain a `config` method to it to change the step's config. -```ts -const myCustom = await helloModuleService - .updateMyCustoms({ - id: "123", - name: "test" - }) +Use the `config` method to change a step's ID for a single execution. -// update multiple -const myCustoms = await helloModuleService - .updateMyCustoms([ - { - id: "123", - name: "test" - }, - { - id: "321", - name: "test 2" - }, - ]) +So, this is the correct way to write the example above: -// use filters -const myCustoms = await helloModuleService - .updateMyCustoms([ - { - selector: { - id: ["123", "321"] - }, - data: { - name: "test" - } - }, - ]) +```ts highlights={highlights} +const helloWorkflow = createWorkflow( + "hello", + () => { + const { data: products } = useQueryGraphStep({ + entity: "product", + fields: ["id"], + }) + + // ✓ No error occurs, the step has a different ID. + const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: ["id"], + }).config({ name: "fetch-customers" }) + } +) ``` -### createMyCustoms +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. -### softDeleteMyCustoms +The first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`. -This method soft-deletes records using an array of IDs or an object of filters. -For example: +# Long-Running Workflows -```ts -await helloModuleService.softDeleteMyCustoms("123") +In this chapter, you’ll learn what a long-running workflow is and how to configure it. -// soft-delete multiple -await helloModuleService.softDeleteMyCustoms([ - "123", "321" -]) +## What is a Long-Running Workflow? -// use filters -await helloModuleService.softDeleteMyCustoms({ - id: ["123", "321"] -}) -``` +When you execute a workflow, you wait until the workflow finishes execution to receive the output. -### updateMyCustoms +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. -### deleteMyCustoms +### Why use Long-Running Workflows? -### softDeleteMyCustoms +Long-running workflows are useful if: -### restoreMyCustoms +- 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. -### Using a Constructor +*** -If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`. +## Configure Long-Running Workflows -For example: +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. -```ts highlights={[["8"]]} -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" +For example, consider the following workflow and steps: -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - constructor() { - super(...arguments) +```ts title="src/workflows/hello-world.ts" highlights={[["15"]]} collapsibleLines="1-11" expandButtonLabel="Show More" +import { + createStep, + createWorkflow, + WorkflowResponse, + StepResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep("step-1", async () => { + return new StepResponse({}) +}) + +const step2 = createStep( + { + name: "step-2", + async: true, + }, + async () => { + console.log("Waiting to be successful...") } -} +) -export default HelloModuleService -``` +const step3 = createStep("step-3", async () => { + return new StepResponse("Finished three steps") +}) +const myWorkflow = createWorkflow( + "hello-world", + function () { + step1() + step2() + const message = step3() -# Service Constraints + return new WorkflowResponse({ + message, + }) +}) -This chapter lists constraints to keep in mind when creating a service. +export default myWorkflow +``` -## Use Async Methods +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. -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. +So, when you execute the `hello-world` workflow, it continues its execution in the background once it reaches the second step. -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: +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 -await helloModuleService.getMessage() -``` +*** -So, make sure your service's methods are always async to avoid unexpected errors or behavior. +## Change Step Status -```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" +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. -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - // Don't - getMessage(): string { - return "Hello, World!" - } +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. - // Do - async getMessage(): Promise<string> { - return "Hello, World!" - } -} +### Retrieve Transaction ID -export default HelloModuleService -``` +Before changing the status of a workflow execution's async step, you must have the execution's transaction ID. +When you execute the workflow, the object returned has a `transaction` property, which is an object that holds the details of the workflow execution's transaction. Use its `transactionId` to later change async steps' statuses: -# Module Options +```ts +const { transaction } = await myWorkflow(req.scope) + .run() -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. +// use transaction.transactionId later +``` -## What are Module Options? +### Change Step Status to Successful -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. +The Workflow Engine Module's main service has a `setStepSuccess` method to set a step's status to successful. If you use it on a workflow execution's async step, the workflow continues execution to the next step. -*** +For example, consider the following step: -## How to Pass Options to a Module? +```ts highlights={successStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + Modules, + TransactionHandlerType, +} from "@medusajs/framework/utils" +import { + StepResponse, + createStep, +} from "@medusajs/framework/workflows-sdk" -To pass options to a module, add an `options` property to the module’s configuration in `medusa-config.ts`. +type SetStepSuccessStepInput = { + transactionId: string +}; -For example: +export const setStepSuccessStep = createStep( + "set-step-success-step", + async function ( + { transactionId }: SetStepSuccessStepInput, + { container } + ) { + const workflowEngineService = container.resolve( + Modules.WORKFLOW_ENGINE + ) -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/hello", + await workflowEngineService.setStepSuccess({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId, + stepId: "step-2", + workflowId: "hello-world", + }, + stepResponse: new StepResponse("Done!"), options: { - capitalize: true, + container, }, - }, - ], -}) + }) + } +) ``` -The `options` property’s value is an object. You can pass any properties you want. +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. -### Pass Options to a Module in a Plugin +The `setStepSuccess` method of the workflow engine's main service accepts as a parameter an object having the following properties: -If your module is part of a plugin, you can pass options to the module in the plugin’s configuration. +- idempotencyKey: (\`object\`) The details of the workflow execution. -For example: + - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. -```ts title="medusa-config.ts" -import { defineConfig } from "@medusajs/framework/utils" -module.exports = defineConfig({ - plugins: [ - { - resolve: "@myorg/plugin-name", - options: { - capitalize: true, - }, - }, - ], -}) -``` + - transactionId: (\`string\`) The ID of the workflow execution's transaction. -The `options` property in the plugin configuration is passed to all modules in a plugin. + - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step. -*** + - 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\<string, any>\`) Options to pass to the step. -## Access Module Options in Main Service + - container: (\`MedusaContainer\`) An instance of the Medusa Container -The module’s main service receives the module options as a second parameter. +### Change Step Status to Failed -For example: +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`. -```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" +After changing the async step's status to failed, the workflow execution fails and the compensation functions of previous steps are executed. -// recommended to define type in another file -type ModuleOptions = { - capitalize?: boolean -} +For example: -export default class HelloModuleService extends MedusaService({ - MyCustom, -}){ - protected options_: ModuleOptions +```ts highlights={failureStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + Modules, + TransactionHandlerType, +} from "@medusajs/framework/utils" +import { + StepResponse, + createStep, +} from "@medusajs/framework/workflows-sdk" - constructor({}, options?: ModuleOptions) { - super(...arguments) +type SetStepFailureStepInput = { + transactionId: string +}; - this.options_ = options || { - capitalize: false, - } - } +export const setStepFailureStep = createStep( + "set-step-success-step", + async function ( + { transactionId }: SetStepFailureStepInput, + { container } + ) { + const workflowEngineService = container.resolve( + Modules.WORKFLOW_ENGINE + ) - // ... -} + await workflowEngineService.setStepFailure({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId, + stepId: "step-2", + workflowId: "hello-world", + }, + stepResponse: new StepResponse("Failed!"), + options: { + container, + }, + }) + } +) ``` +You use this step in another workflow that changes the status of an async step in a long-running workflow's execution to failed. + *** -## Access Module Options in Loader +## Access Long-Running Workflow Status and Result -The object that a module’s loaders receive as a parameter has an `options` property holding the module's options. +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. + +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 example: -```ts title="src/modules/hello/loaders/hello-world.ts" highlights={[["11"], ["12", "ModuleOptions", "The type of expected module options."], ["16"]]} -import { - LoaderOptions, +```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" -// recommended to define type in another file -type ModuleOptions = { - capitalize?: boolean -} +export async function GET(req: MedusaRequest, res: MedusaResponse) { + const { transaction, result } = await myWorkflow(req.scope).run() -export default async function helloWorldLoader({ - options, -}: LoaderOptions<ModuleOptions>) { - - console.log( - "[HELLO MODULE] Just started the Medusa application!", - options + const workflowEngineService = req.scope.resolve< + IWorkflowEngineService + >( + Modules.WORKFLOW_ENGINE ) -} -``` -*** - -## Validate Module Options + const subscriptionOptions = { + workflowId: "hello-world", + transactionId: transaction.transactionId, + subscriberId: "hello-world-subscriber", + } -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. + 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) + } + }, + }) -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. + res.send(result) +} +``` -For example, to validate that the Hello Module received an `apiKey` option, create the loader `src/modules/loaders/validate.ts`: +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. -```ts title="src/modules/hello/loaders/validate.ts" -import { LoaderOptions } from "@medusajs/framework/types" -import { MedusaError } from "@medusajs/framework/utils" +### subscribe Method -// recommended to define type in another file -type ModuleOptions = { - apiKey?: string -} +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 async function validationLoader({ - options, -}: LoaderOptions<ModuleOptions>) { - if (!options.apiKey) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Hello Module requires an apiKey option." - ) - } -} -``` +- 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\<void>\`) 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. -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): +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. -```ts title="src/modules/hello/index.ts" -// other imports... -import validationLoader from "./loaders/validate" +### unsubscribe Method -export default Module("hello", { - // ... - loaders: [validationLoader], -}) -``` +You can unsubscribe from the workflow using the workflow engine's `unsubscribe` method, which requires the same object parameter as the `subscribe` method. -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. +However, instead of the `subscriber` property, it requires a `subscriberOrId` property whose value is the same `subscriberId` passed to the `subscribe` method. +*** -# Scheduled Jobs Number of Executions +## Example: Restaurant-Delivery Recipe -In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. +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). -## numberOfExecutions Option +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. -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: +# Retry Failed Steps -```ts highlights={highlights} -export default async function myCustomJob() { - console.log("I'll be executed three times only.") -} +In this chapter, you’ll learn how to configure steps to allow retrial on failure. -export const config = { - name: "hello-world", - // execute every minute - schedule: "* * * * *", - numberOfExecutions: 3, -} -``` +## Configure a Step’s Retrial -The above scheduled job has the `numberOfExecutions` configuration set to `3`. +By default, when an error occurs in a step, the step and the workflow fail, and the execution stops. -So, it'll only execute 3 times, each every minute, then it won't be executed anymore. +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. -If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. +For example: +```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -# Example: Write Integration Tests for API Routes +const step1 = createStep( + { + name: "step-1", + maxRetries: 2, + }, + async () => { + console.log("Executing step 1") -In this chapter, you'll learn how to write integration tests for API routes using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framework. + throw new Error("Oops! Something happened.") + } +) -### Prerequisites +const myWorkflow = createWorkflow( + "hello-world", + function () { + const str1 = step1() -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) + return new WorkflowResponse({ + message: str1, + }) +}) -## Test a GET API Route +export default myWorkflow +``` -Consider the following API route created at `src/api/custom/route.ts`: +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. -```ts title="src/api/custom/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +When you execute the above workflow, you’ll see the following result in the terminal: -export async function GET( - req: MedusaRequest, - res: MedusaResponse -){ - res.json({ - message: "Hello, World!", - }) -} +```bash +Executing step 1 +Executing step 1 +Executing step 1 +error: Oops! Something happened. +Error: Oops! Something happened. ``` -To write an integration test that tests this API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content: +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. -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={getHighlights} -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +*** -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - describe("GET /custom", () => { - it("returns correct message", async () => { - const response = await api.get( - `/custom` - ) - - expect(response.status).toEqual(200) - expect(response.data).toHaveProperty("message") - expect(response.data.message).toEqual("Hello, World!") - }) - }) - }) - }, -}) +## Step Retry Intervals -jest.setTimeout(60 * 1000) +By default, a step is retried immediately after it fails. To specify a wait time before a step is retried, pass a `retryInterval` property to the step's configuration object. Its value is a number of seconds to wait before retrying the step. + +For example: + +```ts title="src/workflows/hello-world.ts" highlights={[["5"]]} +const step1 = createStep( + { + name: "step-1", + maxRetries: 2, + retryInterval: 2, // 2 seconds + }, + async () => { + // ... + } +) ``` -You use the `medusaIntegrationTestRunner` to write your tests. +### Interval Changes Workflow to Long-Running -You add a single test that sends a `GET` request to `/custom` using the `api.get` method. For the test to pass, the response is expected to: +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. -- Have a code status `200`, -- Have a `message` property in the returned data. -- Have the value of the `message` property equal to `Hello, World!`. +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). -### Run Tests -Run the following command to run your tests: +# Store Workflow Executions -```bash npm2yarn -npm run test:integration -``` +In this chapter, you'll learn how to store workflow executions in the database and access them later. -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). +## Workflow Execution Retention -This runs your Medusa application and runs the tests available under the `src/integrations/http` directory. +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. -### Jest Timeout +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. -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: +You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows. -```ts title="integration-tests/http/custom-routes.spec.ts" -// in your test's file -jest.setTimeout(60 * 1000) +*** + +## How to Store Workflow's Executions? + +### Prerequisites + +- [Redis Workflow Engine must be installed and configured.](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/redis/index.html.md) + +`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: + +- 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. + +For example: + +```ts highlights={highlights} +import { createStep, createWorkflow } from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + { + name: "step-1", + }, + async () => { + console.log("Hello from step 1") + } +) + +export const helloWorkflow = createWorkflow( + { + name: "hello-workflow", + retentionTime: 99999, + store: true, + }, + () => { + step1() + } +) ``` -*** +Whenever you execute the `helloWorkflow` now, its execution details will be stored in the database. -## Test a POST API Route +*** -Suppose you have a `hello` module whose main service extends the service factory, and that has the following model: +## Retrieve Workflow Executions -```ts title="src/modules/hello/models/my-custom.ts" -import { model } from "@medusajs/framework/utils" +You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows. -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), -}) +When you execute a workflow, the returned object has a `transaction` property containing the workflow execution's transaction details: -export default MyCustom +```ts +const { transaction } = await helloWorkflow(container).run() ``` -And consider that the file `src/api/custom/route.ts` defines another route handler for `POST` requests: +To retrieve a workflow's execution details from the database, resolve the Workflow Engine Module from the container and use its `listWorkflowExecutions` method. -```ts title="src/api/custom/route.ts" -// other imports... -import HelloModuleService from "../../../modules/hello/service" +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 title="src/workflows/[id]/route.ts" highlights={retrieveHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework" +import { Modules } from "@medusajs/framework/utils" -export async function POST( +export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const helloModuleService: HelloModuleService = req.scope.resolve( - "helloModuleService" + const { transaction_id } = req.params + + const workflowEngineService = req.scope.resolve( + Modules.WORKFLOW_ENGINE ) - const myCustom = await helloModuleService.createMyCustoms( - req.body - ) + const [workflowExecution] = await workflowEngineService.listWorkflowExecutions({ + transaction_id: transaction_id, + }) res.json({ - my_custom: myCustom, + workflowExecution, }) } ``` -This API route creates a new record of `MyCustom`. - -To write tests for this API route, add the following at the end of the `testSuite` function in `integration-tests/http/custom-routes.spec.ts`: - -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={postHighlights} -// other imports... -import HelloModuleService from "../../src/modules/hello/service" - -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - // other tests... +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. - describe("POST /custom", () => { - const id = "1" +A workflow execution object will be similar to the following: - it("Creates my custom", async () => { - - const response = await api.post( - `/custom`, - { - id, - name: "Test", - } - ) - - expect(response.status).toEqual(200) - expect(response.data).toHaveProperty("my_custom") - expect(response.data.my_custom).toEqual({ - id, - name: "Test", - created_at: expect.any(String), - updated_at: expect.any(String), - }) - }) - }) - }) +```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 +} ``` -This adds a test for the `POST /custom` API route. It uses `api.post` to send the POST request. The `api.post` method accepts as a second parameter the data to pass in the request body. +### Example: Check if Stored Workflow Execution Failed -The test passes if the response has: +To check if a stored workflow execution failed, you can check its `state` property: -- Status code `200`. -- A `my_custom` property in its data. -- Its `id` and `name` match the ones provided to the request. +```ts +if (workflowExecution.state === "failed") { + return res.status(500).json({ + error: "Workflow failed", + }) +} +``` -### Tear Down Created Record +Other state values include `done`, `invoking`, and `compensating`. -To ensure consistency in the database for the rest of the tests after the above test is executed, utilize [Jest's setup and teardown hooks](https://jestjs.io/docs/setup-teardown) to delete the created record. -Use the `getContainer` function passed as a parameter to the `testSuite` function to resolve a service and use it for setup or teardown purposes +# Workflow Timeout -So, add an `afterAll` hook in the `describe` block for `POST /custom`: +In this chapter, you’ll learn how to set a timeout for workflows and steps. -```ts title="integration-tests/http/custom-routes.spec.ts" -// other imports... -import HelloModuleService from "../../src/modules/hello/service" +## What is a Workflow Timeout? -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - // other tests... +By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. - describe("POST /custom", () => { - // ... - afterAll(() => async () => { - const helloModuleService: HelloModuleService = getContainer().resolve( - "helloModuleService" - ) +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. - await helloModuleService.deleteMyCustoms(id) - }) - }) - }) - }, -}) -``` +### Timeout Doesn't Stop Step Execution -The `afterAll` hook resolves the `HelloModuleService` and use its `deleteMyCustoms` to delete the record created by the test. +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. *** -## Test a DELETE API Route +## Configure Workflow Timeout -Consider a `/custom/:id` API route created at `src/api/custom/[id]/route.ts`: +The `createWorkflow` function can accept a configuration object instead of the workflow’s name. -```ts title="src/api/custom/[id]/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import HelloModuleService from "../../../modules/hello/service" +In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. -export async function DELETE( - req: MedusaRequest, - res: MedusaResponse -) { - const helloModuleService: HelloModuleService = req.scope.resolve( - "helloModuleService" - ) +For example: - await helloModuleService.deleteMyCustoms(req.params.id) +```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" +import { + createStep, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" - res.json({ - success: true, +const step1 = createStep( + "step-1", + async () => { + // ... + } +) + +const myWorkflow = createWorkflow({ + name: "hello-world", + timeout: 2, // 2 seconds +}, function () { + const str1 = step1() + + return new WorkflowResponse({ + message: str1, }) -} +}) + +export default myWorkflow + ``` -This API route accepts an ID path parameter, and uses the `HelloModuleService` to delete a `MyCustom` record by that ID. +This workflow's executions fail if they run longer than two seconds. -To add tests for this API route, add the following to `integration-tests/http/custom-routes.spec.ts`: +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`. -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={deleteHighlights} -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - // ... +*** - describe("DELETE /custom/:id", () => { - const id = "1" +## Configure Step Timeout - beforeAll(() => async () => { - const helloModuleService: HelloModuleService = getContainer().resolve( - "helloModuleService" - ) +Alternatively, you can configure the timeout for a step rather than the entire workflow. - await helloModuleService.createMyCustoms({ - id, - name: "Test", - }) - }) +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. - it("Deletes my custom", async () => { - const response = await api.delete( - `/custom/${id}` - ) +The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. - expect(response.status).toEqual(200) - expect(response.data).toHaveProperty("success") - expect(response.data.success).toBeTruthy() - }) - }) - }) +For example: + +```tsx +const step1 = createStep( + { + name: "step-1", + timeout: 2, // 2 seconds }, -}) + async () => { + // ... + } +) ``` -This adds a new test for the `DELETE /custom/:id` API route. You use the `beforeAll` hook to create a `MyCustom` record using the `HelloModuleService`. +This step's executions fail if they run longer than two seconds. -In the test, you use the `api.delete` method to send a `DELETE` request to `/custom/:id`. The test passes if the response: +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`. -- Has a `200` status code. -- Has a `success` property in its data. -- The `success` property's value is true. -*** +# Variable Manipulation in Workflows with transform -## Pass Headers in Test Requests +In this chapter, you'll learn how to use `transform` from the Workflows SDK to manipulate variables in a workflow. -Some requests require passing headers. For example, all routes prefixed with `/store` must pass a publishable API key in the header. +## Why Variable Manipulation isn't Allowed in Workflows -The `get`, `post`, and `delete` methods accept an optional third parameter that you can pass a `headers` property to, whose value is an object of headers to pass in the request. +Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. -### Pass Publishable API Key +At that point, variables in the workflow don't have any values. They only do when you execute the workflow. -For example, to pass a publishable API key in the header for a request to a `/store` route: +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. -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={headersHighlights} -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { ApiKeyDTO } from "@medusajs/framework/types" -import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows" +Instead, use `transform` from the Workflows SDK. -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - let pak: ApiKeyDTO - beforeAll(async () => { - pak = (await createApiKeysWorkflow(getContainer()).run({ - input: { - api_keys: [ - { - type: "publishable", - title: "Test Key", - created_by: "", - }, - ], - }, - })).result[0] - }) - describe("GET /custom", () => { - it("returns correct message", async () => { - const response = await api.get( - `/store/custom`, - { - headers: { - "x-publishable-api-key": pak.token, - }, - } - ) - - expect(response.status).toEqual(200) - expect(response.data).toHaveProperty("message") - expect(response.data.message).toEqual("Hello, World!") - }) - }) - }) - }, -}) +Restrictions for variable manipulation is only applicable in a workflow's definition. You can still manipulate variables in your step's code. -jest.setTimeout(60 * 1000) +*** + +## 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() ``` -In your test suit, you add a `beforeAll` hook to create a publishable API key before the tests run. To create the API key, you can use the `createApiKeysWorkflow` or the [API Key Module's service](https://docs.medusajs.com/resources/commerce-modules/api-key/index.html.md). +To concatenate the strings, you create a new variable `str3` using the `transform` function: -Then, in the test, you pass an object as the last parameter to `api.get` with a `headers` property. The `headers` property is an object with the key `x-publishable-api-key` and the value of the API key's token. +```ts highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + transform, +} from "@medusajs/framework/workflows-sdk" +// step imports... -### Send Authenticated Requests +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str1 = step1(input) + const str2 = step2(input) -If your custom route is accessible by authenticated users only, such as routes prefixed by `/admin` or `/store/customers/me`, you can create a test customer or user, generate a JWT token for them, and pass the token in the request's Authorization header. + const str3 = transform( + { str1, str2 }, + (data) => `${data.str1}${data.str2}` + ) -For example: + return new WorkflowResponse(str3) + } +) +``` -- The `jsonwebtoken` is available in your application by default. -- For custom actor types, you only need to change the `actorType` value in the `jwt.sign` method. +`transform` accepts two parameters: -### Admin User +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. -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={adminHighlights} -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import jwt from "jsonwebtoken" +The value returned by the second parameter function is returned by `transform`. So, the `str3` variable holds the concatenated string. -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - describe("GET /custom", () => { - const headers: Record<string, string> = { - } - beforeEach(async () => { - const container = getContainer() - - const authModuleService = container.resolve("auth") - const userModuleService = container.resolve("user") - - const user = await userModuleService.createUsers({ - email: "admin@medusa.js", - - }) - const authIdentity = await authModuleService.createAuthIdentities({ - provider_identities: [ - { - provider: "emailpass", - entity_id: "admin@medusa.js", - provider_metadata: { - password: "supersecret", - }, - }, - ], - app_metadata: { - user_id: user.id, - }, - }) - - const token = jwt.sign( - { - actor_id: user.id, - actor_type: "user", - auth_identity_id: authIdentity.id, - }, - "supersecret", - { - expiresIn: "1d", - } - ) - - headers["authorization"] = `Bearer ${token}` - }) - it("returns correct message", async () => { - const response = await api.get( - `/admin/custom`, - { headers } - ) - - expect(response.status).toEqual(200) - }) - }) - }) - }, -}) +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. -jest.setTimeout(60 * 1000) -``` +*** -### Customer User +## Example: Looping Over Array -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={customerHighlights} -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { ApiKeyDTO } from "@medusajs/framework/types" -import jwt from "jsonwebtoken" -import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows" +Use `transform` to loop over arrays to create another variable from the array's items. -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - describe("GET /custom", () => { - const headers: Record<string, string> = { - } - beforeEach(async () => { - const container = getContainer() - - const authModuleService = container.resolve("auth") - const customerModuleService = container.resolve("customer") - - const customer = await customerModuleService.createCustomers({ - email: "admin@medusa.js", - - }) - const authIdentity = await authModuleService.createAuthIdentities({ - provider_identities: [ - { - provider: "emailpass", - entity_id: "customer@medusa.js", - provider_metadata: { - password: "supersecret", - }, - }, - ], - app_metadata: { - user_id: customer.id, - }, - }) - - const token = jwt.sign( - { - actor_id: customer.id, - actor_type: "customer", - auth_identity_id: authIdentity.id, - }, - "supersecret", - { - expiresIn: "1d", - } - ) - - headers["authorization"] = `Bearer ${token}` +For example: +```ts collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createWorkflow, + WorkflowResponse, + transform, +} from "@medusajs/framework/workflows-sdk" +// step imports... - const pak = (await createApiKeysWorkflow(getContainer()).run({ - input: { - api_keys: [ - { - type: "publishable", - title: "Test Key", - created_by: "", - }, - ], - }, - })).result[0] +type WorkflowInput = { + items: { + id: string + name: string + }[] +} - headers["x-publishable-api-key"] = pak.token - }) - it("returns correct message", async () => { - const response = await api.get( - `/store/customers/me/custom`, - { headers } - ) - - expect(response.status).toEqual(200) - }) - }) - }) - }, -}) +const myWorkflow = createWorkflow( + "hello-world", + function ({ items }: WorkflowInput) { + const ids = transform( + { items }, + (data) => data.items.map((item) => item.id) + ) + + doSomethingStep(ids) -jest.setTimeout(60 * 1000) + // ... + } +) ``` -In the test suite, you add a `beforeEach` hook that creates a user or customer, an auth identity, and generates a JWT token for them. The JWT token is then set in the `Authorization` header of the request. +This workflow receives an `items` array in its input. -You also create and pass a publishable API key in the header for the customer as it's required for requests to `/store` routes. Learn more in [this section](#pass-publishable-api-key). +You use `transform` to create an `ids` variable, which is an array of strings holding the `id` of each item in the `items` array. + +You then pass the `ids` variable as a parameter to the `doSomethingStep`. *** -## Upload Files in Test Requests +## Example: Creating a Date -If your API route requires uploading a file, create a `FormData` object imported from the `form-data` object, then pass the form data headers in the request. +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. + +So, use `transform` instead to create a date variable with `new Date()`. For example: -The `form-data` package is available by default. +```ts +const myWorkflow = createWorkflow( + "hello-world", + () => { + const today = transform({}, () => new Date()) -```ts title="integration-tests/http/custom-routes.spec.ts" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import FormData from "form-data" + doSomethingStep(today) + } +) +``` -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - describe("GET /custom", () => { - it("upload file", async () => { - const form = new FormData() - form.append("files", Buffer.from("content 1"), "image1.jpg") - form.append("files", Buffer.from("content 2"), "image2.jpg") +In this workflow, `today` is only evaluated when the workflow is executed. - const response = await api.post(`/custom`, form, { - headers: form.getHeaders(), - }) - - expect(response.status).toEqual(200) - expect(response.data).toHaveProperty("files") - expect(response.data.files).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - url: expect.any(String), - }), - ]) - ) - }) - }) - }) - }, -}) +*** -jest.setTimeout(60 * 1000) +## Caveats + +### Transform Evaluation + +`transform`'s value is only evaluated if you pass its output to a step or in the workflow response. + +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}` + ) + + return new WorkflowResponse("done") + } +) ``` -You don't have to actually upload a file, you use the `form.append` method to append to a `files` field in the form data object, and you pass random content using the `Buffer.from` method. +Since `str`'s value isn't used as a step's input or passed to `WorkflowResponse`, its value is never evaluated. -Then, you pass to the `api.post` method the form data object as a second parameter, and an object with the `headers` property set to the form data object's headers as a third parameter. +### Data Validation -If you're passing authentication or other headers, you can pass both the form data headers and the authentication headers in the same object: +`transform` should only be used to perform variable or data manipulation. -```ts title="integration-tests/http/custom-routes.spec.ts" -const response = await api.post(`/custom`, form, { - headers: { - ...form.getHeaders(), - ...authHeaders, - }, -}) +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. + +For example: + +```ts +// DON'T +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str = transform( + { input }, + (data) => { + if (!input.str1) { + throw new Error("Not allowed!") + } + } + ) + } +) + +// DO +const validateHasStr1Step = createStep( + "validate-has-str1", + ({ input }) => { + if (!input.str1) { + throw new Error("Not allowed!") + } + } +) + +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + validateHasStr1({ + input, + }) + + // workflow continues its execution only if + // the step doesn't throw the error. + } +) ``` -# Example: Write Integration Tests for Workflows +# Architectural Modules -In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framwork. +In this chapter, you’ll learn about architectural modules. -### Prerequisites +## What is an Architectural Module? -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) +An architectural module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. -## Write Integration Test for Workflow +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 the following workflow defined at `src/workflows/hello-world.ts`: +*** -```ts title="src/workflows/hello-world.ts" -import { - createWorkflow, - createStep, - StepResponse, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +## Architectural Module Types -const step1 = createStep("step-1", () => { - return new StepResponse("Hello, World!") -}) +There are different architectural module types including: -export const helloWorldWorkflow = createWorkflow( - "hello-world-workflow", - () => { - const message = step1() + - return new WorkflowResponse(message) +- Cache Module: Defines the caching mechanism or logic to cache computational results. +- Event Module: Integrates a pub/sub service to handle subscribing to and emitting events. +- Workflow Engine Module: Integrates a service to store and track workflow executions and steps. +- File Module: Integrates a storage service to handle uploading and managing files. +- Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers. + +*** + +## Architectural Modules List + +Refer to the [Architectural Modules reference](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. + + +# Workflow Hooks + +In this chapter, you'll learn what a workflow hook is and how to consume them. + +## What is a Workflow Hook? + +A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler. + +Medusa exposes hooks in many of its workflows that are used in its API routes. You can consume those hooks to add your custom logic. + +Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks. + +You want to perform a custom action during a workflow's execution, such as when a product is created. + +*** + +## How to Consume a Hook? + +A workflow has a special `hooks` property which is an object that holds its hooks. + +So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory: + +- Import the workflow. +- Access its hook using the `hooks` property. +- Pass the hook a step function as a parameter to consume it. + +For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content: + +```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +createProductsWorkflow.hooks.productsCreated( + async ({ products }, { container }) => { + // TODO perform an action } ) ``` -To write a test for this workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content: +The `productsCreated` hook is available on the workflow's `hooks` property by its name. -```ts title="integration-tests/http/workflow.spec.ts" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { helloWorldWorkflow } from "../../src/workflows/hello-world" +You invoke the hook, passing a step function (the hook handler) as a parameter. -medusaIntegrationTestRunner({ - testSuite: ({ getContainer }) => { - describe("Test hello-world workflow", () => { - it("returns message", async () => { - const { result } = await helloWorldWorkflow(getContainer()) - .run() +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. - expect(result).toEqual("Hello, World!") - }) - }) +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. + +### Hook Handler Parameter + +Since a hook handler is essentially a step function, it receives the hook's input as a first parameter, and an object holding a `container` property as a second parameter. + +Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product. + +### Hook Handler Compensation + +Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook. + +For example: + +```ts title="src/workflows/hooks/product-created.ts" +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +createProductsWorkflow.hooks.productsCreated( + async ({ products }, { container }) => { + // TODO perform an action + + return new StepResponse(undefined, { ids }) }, -}) + async ({ ids }, { container }) => { + // undo the performed action + } +) +``` -jest.setTimeout(60 * 1000) +The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler. + +The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function. + +It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container. + +### Additional Data Property + +Medusa's workflows pass in the hook's input an `additional_data` property: + +```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +createProductsWorkflow.hooks.productsCreated( + async ({ products, additional_data }, { container }) => { + // TODO perform an action + } +) ``` -You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`. +This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow. -### Jest Timeout +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). -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: +### Pass Additional Data to Workflow -```ts title="integration-tests/http/custom-routes.spec.ts" -// in your test's file -jest.setTimeout(60 * 1000) +You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow: + +```ts title="src/workflows/hooks/product-created.ts" highlights={[["10", "additional_data"]]} +import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +export async function POST(req: MedusaRequest, res: MedusaResponse) { + await createProductsWorkflow(req.scope).run({ + input: { + products: [ + // ... + ], + additional_data: { + custom_field: "test", + }, + }, + }) +} ``` +Your hook handler then receives that passed data in the `additional_data` object. + + +# Commerce Modules + +In this chapter, you'll learn about Medusa's commerce modules. + +## What is a Commerce Module? + +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. + +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. + +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) + +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. + +### List of Medusa's Commerce Modules + +Refer to [this reference](https://docs.medusajs.com/resources/commerce-modules/index.html.md) for a full list of commerce modules in Medusa. + *** -## Run Test +## Use Commerce Modules in Custom Flows -Run the following command to run your tests: +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. -```bash npm2yarn -npm run test:integration +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} +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" + +export const countProductsStep = createStep( + "count-products", + async ({ }, { container }) => { + const productModuleService = container.resolve("product") + + const [,count] = await productModuleService.listAndCountProducts() + + return new StepResponse(count) + } +) ``` -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). +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. -This runs your Medusa application and runs the tests available under the `integrations/http` directory. + +# Module Container + +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. + +So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container. + +### List of Registered Resources + +Find a list of resources or dependencies registered in a module's container in [this Development Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). *** -## Test That a Workflow Throws an Error +## Resolve Resources -You might want to test that a workflow throws an error in certain cases. To test this: +### Services -- Disable the `throwOnError` option when executing the workflow. -- Use the returned `errors` property to check what errors were thrown. +A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. -For example, if you have a step that throws this error: +For example: -```ts title="src/workflows/hello-world.ts" -import { MedusaError } from "@medusajs/framework/utils" -import { createStep } from "@medusajs/framework/workflows-sdk" +```ts highlights={[["4"], ["10"]]} +import { Logger } from "@medusajs/framework/types" -const step1 = createStep("step-1", () => { - throw new MedusaError(MedusaError.Types.NOT_FOUND, "Item doesn't exist") -}) +type InjectedDependencies = { + logger: Logger +} + +export default class HelloModuleService { + protected logger_: Logger + + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger + + this.logger_.info("[HelloModuleService]: Hello World!") + } + + // ... +} ``` -You can write the following test to ensure that the workflow throws that error: +### Loader -```ts title="integration-tests/http/workflow.spec.ts" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { helloWorldWorkflow } from "../../src/workflows/hello-world" +A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources. -medusaIntegrationTestRunner({ - testSuite: ({ getContainer }) => { - describe("Test hello-world workflow", () => { - it("returns message", async () => { - const { errors } = await helloWorldWorkflow(getContainer()) - .run({ - throwOnError: false, - }) +For example: + +```ts highlights={[["9"]]} +import { + LoaderOptions, +} from "@medusajs/framework/types" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" - expect(errors.length).toBeGreaterThan(0) - expect(errors[0].error.message).toBe("Item doesn't exist") - }) - }) - }, -}) +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) -jest.setTimeout(60 * 1000) + logger.info("[helloWorldLoader]: Hello, World!") +} ``` -The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, being the error thrown. -If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. +# Perform Database Operations in a Service +In this chapter, you'll learn how to perform database operations in a module's service. -# Example: Integration Tests for a Module +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. -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. +## Run Queries -### Prerequisites +[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. -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) +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). -## Write Integration Test for Module +So, to run database queries in a service: -Consider a `hello` module with a `HelloModuleService` that has a `getMessage` method: +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` -```ts title="src/modules/hello/service.ts" -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" +For example, in your service, add the following methods: -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - getMessage(): string { - return "Hello, World!" +```ts highlights={methodsHighlight} +// other imports... +import { + InjectManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { SqlEntityManager } from "@mikro-orm/knex" + +class HelloModuleService { + // ... + + @InjectManager() + async getCount( + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<number> { + return await sharedContext.manager.count("my_custom") + } + + @InjectManager() + async getCountSql( + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<number> { + const data = await sharedContext.manager.execute( + "SELECT COUNT(*) as num FROM my_custom" + ) + + return parseInt(data[0].num) } } - -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<HelloModuleService>({ - moduleName: HELLO_MODULE, - moduleModels: [MyCustom], - resolve: "./src/modules/hello", - testSuite: ({ service }) => { - describe("HelloModuleService", () => { - it("says hello world", () => { - const message = service.getMessage() +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. - expect(message).toEqual("Hello, World!") - }) - }) - }, -}) +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). -jest.setTimeout(60 * 1000) -``` +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. -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. +Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. *** -## Run Test - -Run the following command to run your module integration tests: +## Execute Operations in Transactions -```bash npm2yarn -npm run test:integration:modules -``` +To wrap database operations in a transaction, you create two methods: -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). +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. -This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. +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: -# Access Workflow Errors +```ts highlights={opHighlights} +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" -In this chapter, you’ll learn how to access errors that occur during a workflow’s execution. +class HelloModuleService { + // ... + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<any> { + const transactionManager = sharedContext.transactionManager + await transactionManager.nativeUpdate( + "my_custom", + { + id: input.id, + }, + { + name: input.name, + } + ) -## How to Access Workflow Errors? + // retrieve again + const updatedRecord = await transactionManager.execute( + `SELECT * FROM my_custom WHERE id = '${input.id}'` + ) -By default, when an error occurs in a workflow, it throws that error, and the execution stops. + return updatedRecord + } -You can configure the workflow to return the errors instead so that you can access and handle them differently. + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ) { + return await this.update_(input, sharedContext) + } +} +``` -For example: +The `HelloModuleService` has two methods: -```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" +- A protected `update_` that performs the database operations inside a transaction. +- A public `update` that executes the transactional protected method. -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result, errors } = await myWorkflow(req.scope) - .run({ - // ... - throwOnError: false, - }) +The shared context's `transactionManager` property holds the transactional entity manager (injected by `InjectTransactionManager`) that you use to perform database operations. - if (errors.length) { - return res.send({ - errors: errors.map((error) => error.error), - }) - } +Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. - res.send(result) -} +### Why Wrap a Transactional Method -``` +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. -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. +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. -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. +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. +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. -# Expose a Workflow Hook +For example, the `update` method could be changed to the following: -In this chapter, you'll learn how to expose a hook in your workflow. +```ts +// other imports... +import { EntityManager } from "@mikro-orm/knex" -## When to Expose a Hook +class HelloModuleService { + // ... + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ) { + const newData = await this.update_(input, sharedContext) -Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow. + await sendNewDataToSystem(newData) -Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead. + return newData + } +} +``` -*** +In this case, only the `update_` method is wrapped in a transaction. The returned value `newData` holds the committed result, which can be used for other operations, such as passed to a `sendNewDataToSystem` method. -## How to Expose a Hook in a Workflow? +### Using Methods in Transactional Methods -To expose a hook in your workflow, use `createHook` from the Workflows SDK. +If your transactional method uses other methods that accept a Medusa context, pass the shared context to those methods. For example: -```ts title="src/workflows/my-workflow/index.ts" highlights={hookHighlights} -import { - createStep, - createHook, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { createProductStep } from "./steps/create-product" - -export const myWorkflow = createWorkflow( - "my-workflow", - function (input) { - const product = createProductStep(input) - const productCreatedHook = createHook( - "productCreated", - { productId: product.id } - ) +```ts +// other imports... +import { EntityManager } from "@mikro-orm/knex" - return new WorkflowResponse(product, { - hooks: [productCreatedHook], - }) +class HelloModuleService { + // ... + @InjectTransactionManager() + protected async anotherMethod( + @MedusaContext() sharedContext?: Context<EntityManager> + ) { + // ... } -) + + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<any> { + this.anotherMethod(sharedContext) + } +} ``` -The `createHook` function accepts two parameters: +You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context. -1. The first is a string indicating the hook's name. You use this to consume the hook later. -2. The second is the input to pass to the hook handler. +The `anotherMethod` now runs in the same transaction as the `update_` method. -The workflow must also pass an object having a `hooks` property as a second parameter to the `WorkflowResponse` constructor. Its value is an array of the workflow's hooks. +*** -### How to Consume the Hook? +## Configure Transactions -To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content: +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. -```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights} -import { myWorkflow } from "../my-workflow" +The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more. -myWorkflow.hooks.productCreated( - async ({ productId }, { container }) => { - // TODO perform an action - } -) -``` +The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction. -The hook is available on the workflow's `hooks` property using its name `productCreated`. +For example, resolve the `baseRepository` in your service's constructor: -You invoke the hook, passing a step function (the hook handler) as a parameter. +### Extending Service Factory +```ts highlights={[["14"]]} +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" +import { DAL } from "@medusajs/framework/types" -# Compensation Function +type InjectedDependencies = { + baseRepository: DAL.RepositoryService +} -In this chapter, you'll learn what a compensation function is and how to add it to a step. +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + protected baseRepository_: DAL.RepositoryService -## What is a Compensation Function + constructor({ baseRepository }: InjectedDependencies) { + super(...arguments) + this.baseRepository_ = baseRepository + } +} -A compensation function rolls back or undoes changes made by a step when an error occurs in the workflow. +export default HelloModuleService +``` -For example, if a step creates a record, the compensation function deletes the record when an error occurs later in the workflow. +### Without Service Factory -By using compensation functions, you provide a mechanism that guarantees data consistency in your application and across systems. +```ts highlights={[["10"]]} +import { DAL } from "@medusajs/framework/types" -*** +type InjectedDependencies = { + baseRepository: DAL.RepositoryService +} -## How to add a Compensation Function? +class HelloModuleService { + protected baseRepository_: DAL.RepositoryService -A compensation function is passed as a second parameter to the `createStep` function. + constructor({ baseRepository }: InjectedDependencies) { + this.baseRepository_ = baseRepository + } +} -For example, create the file `src/workflows/hello-world.ts` with the following content: +export default HelloModuleService +``` -```ts title="src/workflows/hello-world.ts" highlights={[["15"], ["16"], ["17"]]} collapsibleLines="1-5" expandButtonLabel="Show Imports" +Then, add the following method that uses it: + +```ts highlights={repoHighlights} +// ... import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" -const step1 = createStep( - "step-1", - async () => { - const message = `Hello from step one!` +class HelloModuleService { + // ... + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<any> { + return await this.baseRepository_.transaction( + async (transactionManager) => { + await transactionManager.nativeUpdate( + "my_custom", + { + id: input.id, + }, + { + name: input.name, + } + ) - console.log(message) + // retrieve again + const updatedRecord = await transactionManager.execute( + `SELECT * FROM my_custom WHERE id = '${input.id}'` + ) - return new StepResponse(message) - }, - async () => { - console.log("Oops! Rolling back my changes...") + return updatedRecord + }, + { + transaction: sharedContext.transactionManager, + } + ) } -) + + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ) { + return await this.update_(input, sharedContext) + } +} ``` -Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow. +The `update_` method uses the `baseRepository_.transaction` method to wrap a function in a transaction. -*** +The function parameter receives a transactional entity manager as a parameter. Use it to perform the database operations. -## Test the Compensation Function +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. -Create a step in the same `src/workflows/hello-world.ts` file that throws an error: +Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. -```ts title="src/workflows/hello-world.ts" -const step2 = createStep( - "step-2", - async () => { - throw new Error("Throwing an error...") +### Transaction Options + +The second parameter of the `baseRepository_.transaction` method is an object of options that accepts the following properties: + +1. `transaction`: Set the transactional entity manager passed to the function. You must provide this option as explained in the previous section. + +```ts highlights={[["16"]]} +// other imports... +import { EntityManager } from "@mikro-orm/knex" + +class HelloModuleService { + // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<any> { + return await this.baseRepository_.transaction<EntityManager>( + async (transactionManager) => { + // ... + }, + { + transaction: sharedContext.transactionManager, + } + ) } -) +} ``` -Then, create a workflow that uses the steps: +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 title="src/workflows/hello-world.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +```ts highlights={[["19"]]} // other imports... +import { IsolationLevel } from "@mikro-orm/core" -// steps... - -const myWorkflow = createWorkflow( - "hello-world", - function (input) { - const str1 = step1() - step2() +class HelloModuleService { + // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<any> { + return await this.baseRepository_.transaction<EntityManager>( + async (transactionManager) => { + // ... + }, + { + isolationLevel: IsolationLevel.READ_COMMITTED, + } + ) + } +} +``` - return new WorkflowResponse({ - message: str1, - }) -}) +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. -export default myWorkflow +```ts highlights={[["16"]]} +class HelloModuleService { + // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context<EntityManager> + ): Promise<any> { + return await this.baseRepository_.transaction<EntityManager>( + async (transactionManager) => { + // ... + }, + { + enableNestedTransactions: false, + } + ) + } +} ``` -Finally, execute the workflow from an API route: -```ts title="src/api/workflow/route.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import myWorkflow from "../../../workflows/hello-world" +# Loaders -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await myWorkflow(req.scope) - .run() +In this chapter, you’ll learn about loaders and how to use them. - res.send(result) -} -``` +## What is a Loader? -Run the Medusa application and send a `GET` request to `/workflow`: +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. -```bash -curl http://localhost:9000/workflow -``` +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. -In the console, you'll see: +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. -- `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. +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). *** -## Pass Input to Compensation Function +## How to Create a Loader? -If a step creates a record, the compensation function must receive the ID of the record to remove it. +### 1. Implement Loader Function -To pass input to the compensation function, pass a second parameter in the `StepResponse` returned by the step. +You create a loader function in a TypeScript or JavaScript file under a module's `loaders` directory. -For example: +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: -```ts highlights={inputHighlights} -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" + -const step1 = createStep( - "step-1", - async () => { - return new StepResponse( - `Hello from step one!`, - { message: "Oops! Rolling back my changes..." } - ) - }, - async ({ message }) => { - console.log(message) - } -) -``` +Learn how to create a module in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -In this example, the step passes an object as a second parameter to `StepResponse`. +```ts title="src/modules/hello/loaders/hello-world.ts" +import { + LoaderOptions, +} from "@medusajs/framework/types" -The compensation function receives the object and uses its `message` property to log a message. +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const logger = container.resolve("logger") -*** + logger.info("[helloWorldLoader]: Hello, World!") +} +``` -## Resolve Resources from the Medusa Container +The loader file exports an async function, which is the function executed when the application loads. -The compensation function receives an object second parameter. The object has a `container` property that you use to resolve resources from the Medusa container. +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. -For example: +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). -```ts -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +### 2. Export Loader in Module Definition -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 - ) +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. - logger.info(message) - } -) +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], +}) ``` -In this example, you use the `container` property in the second object parameter of the compensation function to resolve the logger. +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. -You then use the logger to log a message. +### 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: -## Handle Errors in Loops +```bash npm2yarn +npm run dev +``` -This feature is only available after [Medusa v2.0.5](https://github.com/medusajs/medusa/releases/tag/v2.0.5). +Then, you'll find the following message logged in the terminal: -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: +```plain +info: [HELLO MODULE] Just started the Medusa application! +``` -```ts -// other imports... -import { promiseAll } from "@medusajs/framework/utils" +This indicates that the loader in the `hello` module ran and logged this message. -type StepInput = { - ids: string[] -} +*** -const step1 = createStep( - "step-1", - async ({ ids }: StepInput, { container }) => { - const erpModuleService = container.resolve( - ERP_MODULE - ) - const prevData: unknown[] = [] +## Example: Register Custom MongoDB Connection - await promiseAll( - ids.map(async (id) => { - const data = await erpModuleService.retrieve(id) +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. - await erpModuleService.delete(id) +Consider your have a MongoDB module that allows you to perform operations on a MongoDB database. - prevData.push(id) - }) - ) +### Prerequisites - return new StepResponse(ids, prevData) - } -) -``` +- [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) -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. +To connect to the database, you create the following loader in your module: -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/modules/mongo/loaders/connection.ts" highlights={loaderHighlights} +import { LoaderOptions } from "@medusajs/framework/types" +import { asValue } from "awilix" +import { MongoClient } from "mongodb" -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: +type ModuleOptions = { + connection_url?: string + db_name?: string +} -```ts highlights={highlights} -try { - await promiseAll( - ids.map(async (id) => { - const data = await erpModuleService.retrieve(id) +export default async function mongoConnectionLoader({ + container, + options, +}: LoaderOptions<ModuleOptions>) { + 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) - await erpModuleService.delete(id) + logger.info("Connected to MongoDB") - prevData.push(id) - }) - ) -} catch (e) { - return StepResponse.permanentFailure( - `An error occurred: ${e}`, - prevData - ) + container.register( + "mongoClient", + asValue(clientDb) + ) + } catch (e) { + logger.error( + `[MONGO MDOULE]: An error occurred while connecting to MongoDB: ${e}` + ) + } } ``` -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. - -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. +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: +```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, + }, + }, + ], +}) +``` -# Conditions in Workflows with When-Then +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: -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. +- `connection_url`: the URL to connect to the MongoDB database. +- `db_name`: The name of the database to connect to. -## Why If-Conditions Aren't Allowed in Workflows? +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. -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. +After creating the client, you register it in the module's container using the container's `register` method. The method accepts two parameters: -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. +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. -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. +### Use Custom Registered Resource in Module's Service -Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code. +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: -## How to use When-Then? +```ts title="src/modules/mongo/service.ts" +import type { Db } from "mongodb" -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. +type InjectedDependencies = { + mongoClient: Db +} -For example: +export default class MongoModuleService { + private mongoClient_: Db -```ts highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - when, -} from "@medusajs/framework/workflows-sdk" -// step imports... + constructor({ mongoClient }: InjectedDependencies) { + this.mongoClient_ = mongoClient + } -const workflow = createWorkflow( - "workflow", - function (input: { - is_active: boolean + async createMovie({ title }: { + title: string }) { + const moviesCol = this.mongoClient_.collection("movie") - const result = when( - input, - (input) => { - return input.is_active - } - ).then(() => { - const stepResult = isActiveStep() - return stepResult + const insertedMovie = await moviesCol.insertOne({ + title, }) - // executed without condition - const anotherStepResult = anotherStep(result) + const movie = await moviesCol.findOne({ + _id: insertedMovie.insertedId, + }) - return new WorkflowResponse( - anotherStepResult - ) + return movie } -) -``` - -In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`. - -### When Parameters - -`when` accepts the following parameters: - -1. The first parameter is either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. -2. The second parameter is a function that returns a boolean indicating whether to execute the action in `then`. - -### Then Parameters -To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. + async deleteMovie(id: string) { + const moviesCol = this.mongoClient_.collection("movie") -The callback function is only executed if `when`'s second parameter function returns a `true` value. + await moviesCol.deleteOne({ + _id: { + equals: id, + }, + }) + } +} +``` -*** +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. -## Implementing If-Else with When-Then +Make sure to export the loader in the module's definition in the `index.ts` file at the root directory of the module: -when-then doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow. +```ts title="src/modules/mongo/index.ts" highlights={[["9"]]} +import { Module } from "@medusajs/framework/utils" +import MongoModuleService from "./service" +import mongoConnectionLoader from "./loaders/connection" -For example: +export const MONGO_MODULE = "mongo" -```ts highlights={ifElseHighlights} -const workflow = createWorkflow( - "workflow", - function (input: { - is_active: boolean - }) { +export default Module(MONGO_MODULE, { + service: MongoModuleService, + loaders: [mongoConnectionLoader], +}) +``` - const isActiveResult = when( - input, - (input) => { - return input.is_active - } - ).then(() => { - return isActiveStep() - }) +### Test it Out - const notIsActiveResult = when( - input, - (input) => { - return !input.is_active - } - ).then(() => { - return notIsActiveStep() - }) +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 +info: Connected to MongoDB ``` -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. +You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database. -*** -## Specify Name for When-Then +# Module Isolation -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: +In this chapter, you'll learn how modules are isolated, and what that means for your custom development. -```ts -const isActiveResult = when( - input, - (input) => { - return input.is_active - } -).then(() => { - return isActiveStep() -}) -``` +- 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. -This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name. +## How are Modules Isolated? -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. +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 pass a name for `when-then` as a first parameter of `when`, whose signature can accept three parameters in this case. For example: +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. -```ts highlights={nameHighlights} -const { isActive } = when( - "check-is-active", - input, - (input) => { - return input.is_active - } -).then(() => { - const isActive = isActiveStep() +*** - return { - isActive, - } -}) -``` +## Why are Modules Isolated -Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters: +Some of the module isolation's benefits include: -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`. +- 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. -The second and third parameters are the same as the parameters you previously passed to `when`. +*** +## How to Extend Data Model of Another Module? -# Workflow Constraints +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). -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. +## How to Use Services of Other Modules? -This creates restrictions related to variable manipulations, using if-conditions, and other constraints. This chapter lists these constraints and provides their alternatives. +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. -## Workflow Constraints +Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. -### No Async Functions +### Example -The function passed to `createWorkflow` can’t be an async function: +For example, consider you have two 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) { - // ... -}) +1. A module that stores and manages brands in your application. +2. A module that integrates a third-party Content Management System (CMS). -// Do -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - // ... -}) -``` +To sync brands from your application to the third-party system, create the following steps: -### No Direct Variable Manipulation +```ts title="Example Steps" highlights={stepsHighlights} +const retrieveBrandsStep = createStep( + "retrieve-brands", + async (_, { container }) => { + const brandModuleService = container.resolve( + "brandModuleService" + ) -You can’t directly manipulate variables within the workflow's constructor function. + const brands = await brandModuleService.listBrands() -Learn more about why you can't manipulate variables [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) + return new StepResponse(brands) + } +) -Instead, use `transform` from the Workflows SDK: +const createBrandsInCmsStep = createStep( + "create-brands-in-cms", + async ({ brands }, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" + ) -```ts highlights={highlights} -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const str1 = step1(input) - const str2 = step2(input) + const cmsBrands = await cmsModuleService.createBrands(brands) - return new WorkflowResponse({ - message: `${str1}${str2}`, - }) -}) + return new StepResponse(cmsBrands, cmsBrands) + }, + async (brands, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" + ) -// Do -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const str1 = step1(input) - const str2 = step2(input) + await cmsModuleService.deleteBrands( + brands.map((brand) => brand.id) + ) + } +) +``` - const result = transform( - { - str1, - str2, - }, - (input) => ({ - message: `${input.str1}${input.str2}`, - }) - ) +The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module. - return new WorkflowResponse(result) -}) +Then, create the following workflow that uses these steps: + +```ts title="Example Workflow" +export const syncBrandsWorkflow = createWorkflow( + "sync-brands", + () => { + const brands = retrieveBrandsStep() + + createBrandsInCmsStep({ brands }) + } +) ``` -### Create Dates in transform +You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. -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. -Instead, create the date using `transform`. +# Modules Directory Structure -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). +In this document, you'll learn about the expected files and directories in your module. -For example: + -```ts highlights={dateHighlights} -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const today = new Date() +## index.ts - return new WorkflowResponse({ - today, - }) -}) +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). -// Do -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const today = transform({}, () => new Date()) +*** - return new WorkflowResponse({ - today, - }) -}) -``` +## service.ts -### No If Conditions +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). -You can't use if-conditions in a workflow. +*** -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) +## Other Directories -Instead, use when-then from the Workflows SDK: +The following directories are optional and their content are explained more in the following chapters: -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - if (input.is_active) { - // perform an action - } -}) +- `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. -// 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 - }) -}) -``` -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). +# Multiple Services in a Module -### No Conditional Operators +In this chapter, you'll learn how to use multiple services in a module. -You can't use conditional operators in a workflow, such as `??` or `||`. +## Module's Main and Internal Services -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) +A module has one main service only, which is the service exported in the module's definition. -Instead, use `transform` to store the desired value in a variable. +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. -### Logical Or (||) Alternative +*** -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = input.message || "Hello" -}) +## How to Add an Internal Service -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" +### 1. Create Service -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = transform( - { - input, - }, - (data) => data.input.message || "hello" - ) -}) +To add an internal service, create it in the `services` directory of your module. + +For example, create the file `src/modules/hello/services/client.ts` with the following content: + +```ts title="src/modules/hello/services/client.ts" +export class ClientService { + async getMessage(): Promise<string> { + return "Hello, World!" + } +} ``` -### Nullish Coalescing (??) Alternative +### 2. Export Service in Index -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = input.message ?? "Hello" -}) +Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" +For example, create the file `src/modules/hello/services/index.ts` with the following content: -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = transform( - { - input, - }, - (data) => data.input.message ?? "hello" - ) -}) +```ts title="src/modules/hello/services/index.ts" +export * from "./client" ``` -### Double Not (!!) Alternative +This exports the `ClientService`. -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - step1({ - isActive: !!input.is_active, - }) -}) +### 3. Resolve Internal Service -// Do +Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders. + +For example, in your main service: + +```ts title="src/modules/hello/service.ts" highlights={[["5"], ["13"]]} // other imports... -import { transform } from "@medusajs/framework/workflows-sdk" +import { ClientService } from "./services" -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const isActive = transform( - { - input, - }, - (data) => !!data.input.is_active - ) - - step1({ - isActive, - }) -}) +type InjectedDependencies = { + clientService: ClientService +} + +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + protected clientService_: ClientService + + constructor({ clientService }: InjectedDependencies) { + super(...arguments) + this.clientService_ = clientService + } +} ``` -### Ternary Alternative +You can now use your internal service in your main service. -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - step1({ - message: input.is_active ? "active" : "inactive", - }) -}) +*** -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" +## Resolve Resources in Internal Service -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = transform( - { - input, - }, - (data) => { - return data.input.is_active ? "active" : "inactive" - } - ) - - step1({ - message, - }) -}) -``` +Resolve dependencies from your module's container in the constructor of your internal service. -### Optional Chaining (?.) Alternative +For example: ```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - step1({ - name: input.customer?.name, - }) -}) +import { Logger } from "@medusajs/framework/types" -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" +type InjectedDependencies = { + logger: Logger +} -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const name = transform( - { - input, - }, - (data) => data.input.customer?.name - ) - - step1({ - name, - }) -}) +export class ClientService { + protected logger_: Logger + + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger + } +} ``` *** -## Step Constraints +## Access Module Options -### Returned Values +Your internal service can't access the module's options. -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 retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`. -Values of other types, such as Maps, aren't allowed. +For example: ```ts -// Don't -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" +import { ConfigModule } from "@medusajs/framework/types" +import { HELLO_MODULE } from ".." -const step1 = createStep( - "step-1", - (input, { container }) => { - const myMap = new Map() +export type InjectedDependencies = { + configModule: ConfigModule +} - // ... +export class ClientService { + protected options: Record<string, any> - return new StepResponse({ - myMap, - }) + constructor({ configModule }: InjectedDependencies) { + const moduleDef = configModule.modules[HELLO_MODULE] + + if (typeof moduleDef !== "boolean") { + this.options = moduleDef.options + } } -) +} +``` -// Do -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" +The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. -const step1 = createStep( - "step-1", - (input, { container }) => { - const myObj: Record<string, unknown> = {} +If its value is not a `boolean`, set the service's options to the module configuration's `options` property. - // ... - return new StepResponse({ - myObj, - }) - } -) -``` +# Service Constraints +This chapter lists constraints to keep in mind when creating a service. -# Long-Running Workflows +## Use Async Methods -In this chapter, you’ll learn what a long-running workflow is and how to configure it. +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. -## What is a Long-Running Workflow? +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: -When you execute a workflow, you wait until the workflow finishes execution to receive the output. +```ts +await helloModuleService.getMessage() +``` -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. +So, make sure your service's methods are always async to avoid unexpected errors or behavior. -### Why use Long-Running Workflows? +```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" -Long-running workflows are useful if: +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + // Don't + getMessage(): string { + return "Hello, World!" + } -- 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. + // Do + async getMessage(): Promise<string> { + return "Hello, World!" + } +} -*** +export default HelloModuleService +``` -## Configure Long-Running Workflows -A workflow is considered long-running if at least one step has its `async` configuration set to `true` and doesn't return a step response. +# Module Options -For example, consider the following workflow and steps: +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. -```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" +## What are Module Options? -const step1 = createStep("step-1", async () => { - return new StepResponse({}) -}) +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. -const step2 = createStep( - { - name: "step-2", - async: true, - }, - async () => { - console.log("Waiting to be successful...") - } -) +*** -const step3 = createStep("step-3", async () => { - return new StepResponse("Finished three steps") -}) +## How to Pass Options to a Module? -const myWorkflow = createWorkflow( - "hello-world", - function () { - step1() - step2() - const message = step3() +To pass options to a module, add an `options` property to the module’s configuration in `medusa-config.ts`. - return new WorkflowResponse({ - message, - }) -}) +For example: -export default myWorkflow +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/hello", + options: { + capitalize: true, + }, + }, + ], +}) ``` -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. +The `options` property’s value is an object. You can pass any properties you want. -So, when you execute the `hello-world` workflow, it continues its execution in the background once it reaches the second step. +### Pass Options to a Module in a Plugin -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). +If your module is part of a plugin, you can pass options to the module in the plugin’s configuration. + +For example: + +```ts title="medusa-config.ts" +import { defineConfig } from "@medusajs/framework/utils" +module.exports = defineConfig({ + plugins: [ + { + resolve: "@myorg/plugin-name", + options: { + capitalize: true, + }, + }, + ], +}) +``` + +The `options` property in the plugin configuration is passed to all modules in a plugin. *** -## Change Step Status +## Access Module Options in Main Service -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. +The module’s main service receives the module options as a second parameter. -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. +For example: -### Retrieve Transaction ID +```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" -Before changing the status of a workflow execution's async step, you must have the execution's transaction ID. +// recommended to define type in another file +type ModuleOptions = { + capitalize?: boolean +} -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: +export default class HelloModuleService extends MedusaService({ + MyCustom, +}){ + protected options_: ModuleOptions -```ts -const { transaction } = await myWorkflow(req.scope) - .run() + constructor({}, options?: ModuleOptions) { + super(...arguments) -// use transaction.transactionId later + this.options_ = options || { + capitalize: false, + } + } + + // ... +} ``` -### 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. +## Access Module Options in Loader -For example, consider the following step: +The object that a module’s loaders receive as a parameter has an `options` property holding the module's options. -```ts highlights={successStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" +For example: + +```ts title="src/modules/hello/loaders/hello-world.ts" highlights={[["11"], ["12", "ModuleOptions", "The type of expected module options."], ["16"]]} import { - Modules, - TransactionHandlerType, -} from "@medusajs/framework/utils" -import { - StepResponse, - createStep, -} from "@medusajs/framework/workflows-sdk" + LoaderOptions, +} from "@medusajs/framework/types" + +// recommended to define type in another file +type ModuleOptions = { + capitalize?: boolean +} + +export default async function helloWorldLoader({ + options, +}: LoaderOptions<ModuleOptions>) { + + console.log( + "[HELLO MODULE] Just started the Medusa application!", + options + ) +} +``` + +*** + +## Validate Module Options -type SetStepSuccessStepInput = { - transactionId: string -}; +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. -export const setStepSuccessStep = createStep( - "set-step-success-step", - async function ( - { transactionId }: SetStepSuccessStepInput, - { container } - ) { - const workflowEngineService = container.resolve( - Modules.WORKFLOW_ENGINE - ) +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. - await workflowEngineService.setStepSuccess({ - idempotencyKey: { - action: TransactionHandlerType.INVOKE, - transactionId, - stepId: "step-2", - workflowId: "hello-world", - }, - stepResponse: new StepResponse("Done!"), - options: { - container, - }, - }) +For example, to validate that the Hello Module received an `apiKey` option, create the loader `src/modules/loaders/validate.ts`: + +```ts title="src/modules/hello/loaders/validate.ts" +import { LoaderOptions } from "@medusajs/framework/types" +import { MedusaError } from "@medusajs/framework/utils" + +// recommended to define type in another file +type ModuleOptions = { + apiKey?: string +} + +export default async function validationLoader({ + options, +}: LoaderOptions<ModuleOptions>) { + if (!options.apiKey) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Hello Module requires an apiKey option." + ) } -) +} ``` -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. +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): -The `setStepSuccess` method of the workflow engine's main service accepts as a parameter an object having the following properties: +```ts title="src/modules/hello/index.ts" +// other imports... +import validationLoader from "./loaders/validate" -- idempotencyKey: (\`object\`) The details of the workflow execution. +export default Module("hello", { + // ... + loaders: [validationLoader], +}) +``` - - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. +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. - - transactionId: (\`string\`) The ID of the workflow execution's transaction. - - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step. +# Service Factory - - 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\<string, any>\`) Options to pass to the step. +In this chapter, you’ll learn about what the service factory is and how to use it. - - container: (\`MedusaContainer\`) An instance of the Medusa Container +## What is the Service Factory? -### Change Step Status to Failed +Medusa provides a service factory that your module’s main service can extend. -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`. +The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually. -After changing the async step's status to failed, the workflow execution fails and the compensation functions of previous steps are executed. +Your service provides data-management functionalities of your data models. -For example: +*** -```ts highlights={failureStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - Modules, - TransactionHandlerType, -} from "@medusajs/framework/utils" -import { - StepResponse, - createStep, -} from "@medusajs/framework/workflows-sdk" +## How to Extend the Service Factory? -type SetStepFailureStepInput = { - transactionId: string -}; +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. -export const setStepFailureStep = createStep( - "set-step-success-step", - async function ( - { transactionId }: SetStepFailureStepInput, - { container } - ) { - const workflowEngineService = container.resolve( - Modules.WORKFLOW_ENGINE - ) +For example, create the file `src/modules/hello/service.ts` with the following content: - await workflowEngineService.setStepFailure({ - idempotencyKey: { - action: TransactionHandlerType.INVOKE, - transactionId, - stepId: "step-2", - workflowId: "hello-world", - }, - stepResponse: new StepResponse("Failed!"), - options: { - container, - }, - }) - } -) +```ts title="src/modules/hello/service.ts" highlights={highlights} +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" + +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + // TODO implement custom methods +} + +export default HelloModuleService ``` -You use this step in another workflow that changes the status of an async step in a long-running workflow's execution to failed. +### MedusaService Parameters -*** +The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for. -## Access Long-Running Workflow Status and Result +In the example above, since the `HelloModuleService` extends `MedusaService`, it has methods to manage the `MyCustom` data model, such as `createMyCustoms`. -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. +### Generated Methods -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). +The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database. -For example: +The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`. -```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" +For example, the following methods are generated for the service above: -export async function GET(req: MedusaRequest, res: MedusaResponse) { - const { transaction, result } = await myWorkflow(req.scope).run() +Find a complete reference of each of the methods in [this documentation](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) - const workflowEngineService = req.scope.resolve< - IWorkflowEngineService - >( - Modules.WORKFLOW_ENGINE - ) +### listMyCustoms - const subscriptionOptions = { - workflowId: "hello-world", - transactionId: transaction.transactionId, - subscriberId: "hello-world-subscriber", - } +### listMyCustoms - 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) - } - }, - }) +This method retrieves an array of records based on filters and pagination configurations. - res.send(result) -} -``` +For example: -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. +```ts +const myCustoms = await helloModuleService + .listMyCustoms() -### subscribe Method +// with filters +const myCustoms = await helloModuleService + .listMyCustoms({ + id: ["123"] + }) +``` -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: +### listAndCount -- 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\<void>\`) 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. +### retrieveMyCustom -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. +This method retrieves a record by its ID. -### unsubscribe Method +For example: -You can unsubscribe from the workflow using the workflow engine's `unsubscribe` method, which requires the same object parameter as the `subscribe` method. +```ts +const myCustom = await helloModuleService + .retrieveMyCustom("123") +``` -However, instead of the `subscriber` property, it requires a `subscriberOrId` property whose value is the same `subscriberId` passed to the `subscribe` method. +### retrieveMyCustom -*** +### updateMyCustoms -## Example: Restaurant-Delivery Recipe +This method updates and retrieves records of the data model. -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: -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 +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" + }, + ]) -# Execute Another Workflow +// use filters +const myCustoms = await helloModuleService + .updateMyCustoms([ + { + selector: { + id: ["123", "321"] + }, + data: { + name: "test" + } + }, + ]) +``` -In this chapter, you'll learn how to execute a workflow in another. +### createMyCustoms -## Execute in a Workflow +### softDeleteMyCustoms -To execute a workflow in another, use the `runAsStep` method that every workflow has. +This method soft-deletes records using an array of IDs or an object of filters. 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 +await helloModuleService.softDeleteMyCustoms("123") -const workflow = createWorkflow( - "hello-world", - async (input) => { - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - // ... - ], - }, - }) +// soft-delete multiple +await helloModuleService.softDeleteMyCustoms([ + "123", "321" +]) - // ... - } -) +// use filters +await helloModuleService.softDeleteMyCustoms({ + id: ["123", "321"] +}) ``` -Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. +### updateMyCustoms -The object has an `input` property to pass input to the workflow. +### deleteMyCustoms -*** +### softDeleteMyCustoms -## Preparing Input Data +### restoreMyCustoms -If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. +### Using a Constructor -Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). +If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`. For example: -```ts highlights={transformHighlights} collapsibleLines="1-12" -import { - createWorkflow, - transform, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" +```ts highlights={[["8"]]} +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" -type WorkflowInput = { - title: string +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + constructor() { + super(...arguments) + } } -const workflow = createWorkflow( - "hello-product", - async (input: WorkflowInput) => { - const createProductsData = transform({ - input, - }, (data) => [ - { - title: `Hello ${data.input.title}`, - }, - ]) +export default HelloModuleService +``` - const products = createProductsWorkflow.runAsStep({ - input: { - products: createProductsData, - }, - }) - // ... - } -) -``` +# Write Integration Tests -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`. +In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests. -*** +### Prerequisites -## Run Workflow Conditionally +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -To run a workflow in another based on a condition, use when-then from the Workflows SDK. +## medusaIntegrationTestRunner Utility -Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). +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: -```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" +```ts title="integration-tests/http/test.spec.ts" highlights={highlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -type WorkflowInput = { - product?: CreateProductWorkflowInputDTO - should_create?: boolean -} +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + // TODO write tests... + }, +}) -const workflow = createWorkflow( - "hello-product", - async (input: WorkflowInput) => { - const product = when(input, ({ should_create }) => should_create) - .then(() => { - return createProductsWorkflow.runAsStep({ - input: { - products: [input.product], - }, - }) - }) - } -) +jest.setTimeout(60 * 1000) ``` -In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. +The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`. +`testSuite`'s value is a function that defines the tests to run. The function accepts as a parameter an object that has the following properties: -# Multiple Step Usage in Workflow +- `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. -In this chapter, you'll learn how to use a step multiple times in a workflow. +The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). -## Problem Reusing a Step in a Workflow +### Jest Timeout -In some cases, you may need to use a step multiple times in the same workflow. +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: -The most common example is using the `useQueryGraphStep` multiple times in a workflow to retrieve multiple unrelated data, such as customers and products. +```ts title="integration-tests/http/test.spec.ts" +// in your test's file +jest.setTimeout(60 * 1000) +``` -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" - // ... -) +### Run Tests + +Run the following command to run your tests: + +```bash npm2yarn +npm run test:integration ``` -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: +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 -const helloWorkflow = createWorkflow( - "hello", - () => { - const { data: products } = useQueryGraphStep({ - entity: "product", - fields: ["id"], - }) +This runs your Medusa application and runs the tests available under the `src/integrations/http` directory. - // ERROR OCCURS HERE: A STEP HAS THE SAME ID AS ANOTHER IN THE WORKFLOW - const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: ["id"], - }) - } -) -``` +*** -The next section explains how to fix this issue to use the same step multiple times in a workflow. +## Other Options and Inputs + +Refer to [this reference in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. *** -## How to Use a Step Multiple Times in a Workflow? +## Database Used in Tests -When you execute a step in a workflow, you can chain a `config` method to it to change the step's config. +The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. -Use the `config` method to change a step's ID for a single execution. +To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md). -So, this is the correct way to write the example above: +*** -```ts highlights={highlights} -const helloWorkflow = createWorkflow( - "hello", - () => { - const { data: products } = useQueryGraphStep({ - entity: "product", - fields: ["id"], - }) +## Example Integration Tests - // ✓ No error occurs, the step has a different ID. - const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: ["id"], - }).config({ name: "fetch-customers" }) - } -) -``` +The next chapters provide examples of writing integration tests for API routes and workflows. -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`. +# Write Tests for Modules +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. -# Run Workflow Steps in Parallel +### Prerequisites -In this chapter, you’ll learn how to run workflow steps in parallel. +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -## parallelize Utility Function +## moduleIntegrationTestRunner Utility -If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK. +`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. -The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step. +For example, assuming you have a `hello` module, create a test file at `src/modules/hello/__tests__/service.spec.ts`: -For example: +```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" -```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" +moduleIntegrationTestRunner<HelloModuleService>({ + moduleName: HELLO_MODULE, + moduleModels: [MyCustom], + resolve: "./src/modules/hello", + testSuite: ({ service }) => { + // TODO write tests + }, +}) -interface WorkflowInput { - title: string -} +jest.setTimeout(60 * 1000) +``` -const myWorkflow = createWorkflow( - "my-workflow", - (input: WorkflowInput) => { - const product = createProductStep(input) +The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties: - const [prices, productSalesChannel] = parallelize( - createPricesStep(product), - attachProductToSalesChannelStep(product) - ) +- `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. - const id = product.id - const refetchedProduct = getProductStep(product.id) +The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service. - return new WorkflowResponse(refetchedProduct) - } -) -``` +The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property. -The `parallelize` function accepts the steps to run in parallel as a parameter. +The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). -It returns an array of the steps' results in the same order they're passed to the `parallelize` function. +*** -So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`. +## Run Tests +Run the following command to run your module integration tests: -# Retry Failed Steps +```bash npm2yarn +npm run test:integration:modules +``` -In this chapter, you’ll learn how to configure steps to allow retrial on failure. +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). -## Configure a Step’s Retrial +This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. -By default, when an error occurs in a step, the step and the workflow fail, and the execution stops. +*** -You can configure the step to retry on failure. The `createStep` function can accept a configuration object instead of the step’s name as a first parameter. +## Pass Module Options + +If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter. For example: -```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - createStep, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +```ts +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import HelloModuleService from "../service" -const step1 = createStep( - { - name: "step-1", - maxRetries: 2, +moduleIntegrationTestRunner<HelloModuleService>({ + moduleOptions: { + apiKey: "123", }, - async () => { - console.log("Executing step 1") + // ... +}) +``` - throw new Error("Oops! Something happened.") - } -) +*** -const myWorkflow = createWorkflow( - "hello-world", - function () { - const str1 = step1() +## Write Tests for Modules without Data Models - return new WorkflowResponse({ - message: str1, - }) -}) +If your module doesn't have a data model, pass a dummy model in the `moduleModels` property. -export default myWorkflow -``` +For example: -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. +```ts +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import HelloModuleService from "../service" +import { model } from "@medusajs/framework/utils" -When you execute the above workflow, you’ll see the following result in the terminal: +const DummyModel = model.define("dummy_model", { + id: model.id().primaryKey(), +}) -```bash -Executing step 1 -Executing step 1 -Executing step 1 -error: Oops! Something happened. -Error: Oops! Something happened. -``` +moduleIntegrationTestRunner<HelloModuleService>({ + moduleModels: [DummyModel], + // ... +}) -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. +jest.setTimeout(60 * 1000) +``` *** -## Step Retry Intervals +### Other Options and Inputs -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. +Refer to [this reference in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. -For example: +*** -```ts title="src/workflows/hello-world.ts" highlights={[["5"]]} -const step1 = createStep( - { - name: "step-1", - maxRetries: 2, - retryInterval: 2, // 2 seconds - }, - async () => { - // ... - } -) -``` +## Database Used in Tests -### Interval Changes Workflow to Long-Running +The `moduleIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. -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. +To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). -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). +# Example: Write Integration Tests for API Routes -# Store Workflow Executions +In this chapter, you'll learn how to write integration tests for API routes using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framework. -In this chapter, you'll learn how to store workflow executions in the database and access them later. +### Prerequisites -## Workflow Execution Retention +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -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. +## Test a GET API Route -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. +Consider the following API route created at `src/api/custom/route.ts`: -You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows. +```ts title="src/api/custom/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -*** +export async function GET( + req: MedusaRequest, + res: MedusaResponse +){ + res.json({ + message: "Hello, World!", + }) +} +``` -## How to Store Workflow's Executions? +To write an integration test that tests this API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content: -### Prerequisites +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={getHighlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -- [Redis Workflow Engine must be installed and configured.](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/redis/index.html.md) +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + describe("GET /custom", () => { + it("returns correct message", async () => { + const response = await api.get( + `/custom` + ) + + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("message") + expect(response.data.message).toEqual("Hello, World!") + }) + }) + }) + }, +}) -`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: +jest.setTimeout(60 * 1000) +``` -- 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. +You use the `medusaIntegrationTestRunner` to write your tests. -For example: +You add a single test that sends a `GET` request to `/custom` using the `api.get` method. For the test to pass, the response is expected to: -```ts highlights={highlights} -import { createStep, createWorkflow } from "@medusajs/framework/workflows-sdk" +- Have a code status `200`, +- Have a `message` property in the returned data. +- Have the value of the `message` property equal to `Hello, World!`. -const step1 = createStep( - { - name: "step-1", - }, - async () => { - console.log("Hello from step 1") - } -) +### Run Tests -export const helloWorkflow = createWorkflow( - { - name: "hello-workflow", - retentionTime: 99999, - store: true, - }, - () => { - step1() - } -) +Run the following command to run your tests: + +```bash npm2yarn +npm run test:integration ``` -Whenever you execute the `helloWorkflow` now, its execution details will be stored in the database. +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. + +### 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/custom-routes.spec.ts" +// in your test's file +jest.setTimeout(60 * 1000) +``` *** -## Retrieve Workflow Executions +## Test a POST API Route -You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows. +Suppose you have a `hello` module whose main service extends the service factory, and that has the following model: -When you execute a workflow, the returned object has a `transaction` property containing the workflow execution's transaction details: +```ts title="src/modules/hello/models/my-custom.ts" +import { model } from "@medusajs/framework/utils" -```ts -const { transaction } = await helloWorkflow(container).run() +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), +}) + +export default MyCustom ``` -To retrieve a workflow's execution details from the database, resolve the Workflow Engine Module from the container and use its `listWorkflowExecutions` method. +And consider that the file `src/api/custom/route.ts` defines another route handler for `POST` requests: -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 title="src/api/custom/route.ts" +// other imports... +import HelloModuleService from "../../../modules/hello/service" -```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( +export async function POST( req: MedusaRequest, res: MedusaResponse ) { - const { transaction_id } = req.params - - const workflowEngineService = req.scope.resolve( - Modules.WORKFLOW_ENGINE + const helloModuleService: HelloModuleService = req.scope.resolve( + "helloModuleService" ) - const [workflowExecution] = await workflowEngineService.listWorkflowExecutions({ - transaction_id: transaction_id, - }) + const myCustom = await helloModuleService.createMyCustoms( + req.body + ) res.json({ - workflowExecution, + my_custom: myCustom, }) } ``` -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. +This API route creates a new record of `MyCustom`. -A workflow execution object will be similar to the following: +To write tests for this API route, add the following at the end of the `testSuite` function in `integration-tests/http/custom-routes.spec.ts`: -```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 -} -``` +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={postHighlights} +// other imports... +import HelloModuleService from "../../src/modules/hello/service" -### Example: Check if Stored Workflow Execution Failed +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + // other tests... -To check if a stored workflow execution failed, you can check its `state` property: + describe("POST /custom", () => { + const id = "1" -```ts -if (workflowExecution.state === "failed") { - return res.status(500).json({ - error: "Workflow failed", - }) -} + it("Creates my custom", async () => { + + const response = await api.post( + `/custom`, + { + id, + name: "Test", + } + ) + + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("my_custom") + expect(response.data.my_custom).toEqual({ + id, + name: "Test", + created_at: expect.any(String), + updated_at: expect.any(String), + }) + }) + }) + }) + }, +}) ``` -Other state values include `done`, `invoking`, and `compensating`. +This adds a test for the `POST /custom` API route. It uses `api.post` to send the POST request. The `api.post` method accepts as a second parameter the data to pass in the request body. +The test passes if the response has: -# Variable Manipulation in Workflows with transform +- Status code `200`. +- A `my_custom` property in its data. +- Its `id` and `name` match the ones provided to the request. -In this chapter, you'll learn how to use `transform` from the Workflows SDK to manipulate variables in a workflow. +### Tear Down Created Record -## Why Variable Manipulation isn't Allowed in Workflows +To ensure consistency in the database for the rest of the tests after the above test is executed, utilize [Jest's setup and teardown hooks](https://jestjs.io/docs/setup-teardown) to delete the created record. -Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. +Use the `getContainer` function passed as a parameter to the `testSuite` function to resolve a service and use it for setup or teardown purposes -At that point, variables in the workflow don't have any values. They only do when you execute the workflow. +So, add an `afterAll` hook in the `describe` block for `POST /custom`: -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. +```ts title="integration-tests/http/custom-routes.spec.ts" +// other imports... +import HelloModuleService from "../../src/modules/hello/service" -Instead, use `transform` from the Workflows SDK. +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + // other tests... -Restrictions for variable manipulation is only applicable in a workflow's definition. You can still manipulate variables in your step's code. + describe("POST /custom", () => { + // ... + afterAll(() => async () => { + const helloModuleService: HelloModuleService = getContainer().resolve( + "helloModuleService" + ) + + await helloModuleService.deleteMyCustoms(id) + }) + }) + }) + }, +}) +``` + +The `afterAll` hook resolves the `HelloModuleService` and use its `deleteMyCustoms` to delete the record created by the test. *** -## What is the transform Utility? +## Test a DELETE API Route -`transform` creates a new variable as the result of manipulating other variables. +Consider a `/custom/:id` API route created at `src/api/custom/[id]/route.ts`: -For example, consider you have two strings as the output of two steps: +```ts title="src/api/custom/[id]/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import HelloModuleService from "../../../modules/hello/service" -```ts -const str1 = step1() -const str2 = step2() +export async function DELETE( + req: MedusaRequest, + res: MedusaResponse +) { + const helloModuleService: HelloModuleService = req.scope.resolve( + "helloModuleService" + ) + + await helloModuleService.deleteMyCustoms(req.params.id) + + res.json({ + success: true, + }) +} ``` -To concatenate the strings, you create a new variable `str3` using the `transform` function: +This API route accepts an ID path parameter, and uses the `HelloModuleService` to delete a `MyCustom` record by that ID. -```ts highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - transform, -} from "@medusajs/framework/workflows-sdk" -// step imports... +To add tests for this API route, add the following to `integration-tests/http/custom-routes.spec.ts`: -const myWorkflow = createWorkflow( - "hello-world", - function (input) { - const str1 = step1(input) - const str2 = step2(input) +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={deleteHighlights} +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + // ... - const str3 = transform( - { str1, str2 }, - (data) => `${data.str1}${data.str2}` - ) + describe("DELETE /custom/:id", () => { + const id = "1" - return new WorkflowResponse(str3) - } -) -``` + beforeAll(() => async () => { + const helloModuleService: HelloModuleService = getContainer().resolve( + "helloModuleService" + ) -`transform` accepts two parameters: + await helloModuleService.createMyCustoms({ + id, + name: "Test", + }) + }) -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. + it("Deletes my custom", async () => { + const response = await api.delete( + `/custom/${id}` + ) -The value returned by the second parameter function is returned by `transform`. So, the `str3` variable holds the concatenated string. + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("success") + expect(response.data.success).toBeTruthy() + }) + }) + }) + }, +}) +``` -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. +This adds a new test for the `DELETE /custom/:id` API route. You use the `beforeAll` hook to create a `MyCustom` record using the `HelloModuleService`. + +In the test, you use the `api.delete` method to send a `DELETE` request to `/custom/:id`. The test passes if the response: + +- Has a `200` status code. +- Has a `success` property in its data. +- The `success` property's value is true. *** -## Example: Looping Over Array +## Pass Headers in Test Requests -Use `transform` to loop over arrays to create another variable from the array's items. +Some requests require passing headers. For example, all routes prefixed with `/store` must pass a publishable API key in the header. -For example: +The `get`, `post`, and `delete` methods accept an optional third parameter that you can pass a `headers` property to, whose value is an object of headers to pass in the request. -```ts collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - createWorkflow, - WorkflowResponse, - transform, -} from "@medusajs/framework/workflows-sdk" -// step imports... +### Pass Publishable API Key -type WorkflowInput = { - items: { - id: string - name: string - }[] -} +For example, to pass a publishable API key in the header for a request to a `/store` route: -const myWorkflow = createWorkflow( - "hello-world", - function ({ items }: WorkflowInput) { - const ids = transform( - { items }, - (data) => data.items.map((item) => item.id) - ) - - doSomethingStep(ids) +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={headersHighlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { ApiKeyDTO } from "@medusajs/framework/types" +import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows" + +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + let pak: ApiKeyDTO + beforeAll(async () => { + pak = (await createApiKeysWorkflow(getContainer()).run({ + input: { + api_keys: [ + { + type: "publishable", + title: "Test Key", + created_by: "", + }, + ], + }, + })).result[0] + }) + describe("GET /custom", () => { + it("returns correct message", async () => { + const response = await api.get( + `/store/custom`, + { + headers: { + "x-publishable-api-key": pak.token, + }, + } + ) + + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("message") + expect(response.data.message).toEqual("Hello, World!") + }) + }) + }) + }, +}) - // ... - } -) +jest.setTimeout(60 * 1000) ``` -This workflow receives an `items` array in its input. +In your test suit, you add a `beforeAll` hook to create a publishable API key before the tests run. To create the API key, you can use the `createApiKeysWorkflow` or the [API Key Module's service](https://docs.medusajs.com/resources/commerce-modules/api-key/index.html.md). -You use `transform` to create an `ids` variable, which is an array of strings holding the `id` of each item in the `items` array. +Then, in the test, you pass an object as the last parameter to `api.get` with a `headers` property. The `headers` property is an object with the key `x-publishable-api-key` and the value of the API key's token. -You then pass the `ids` variable as a parameter to the `doSomethingStep`. +### Send Authenticated Requests -*** +If your custom route is accessible by authenticated users only, such as routes prefixed by `/admin` or `/store/customers/me`, you can create a test customer or user, generate a JWT token for them, and pass the token in the request's Authorization header. -## Example: Creating a Date +For example: -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. +- The `jsonwebtoken` is available in your application by default. +- For custom actor types, you only need to change the `actorType` value in the `jwt.sign` method. -So, use `transform` instead to create a date variable with `new Date()`. +### Admin User -For example: +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={adminHighlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import jwt from "jsonwebtoken" -```ts -const myWorkflow = createWorkflow( - "hello-world", - () => { - const today = transform({}, () => new Date()) +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + describe("GET /custom", () => { + const headers: Record<string, string> = { + } + beforeEach(async () => { + const container = getContainer() + + const authModuleService = container.resolve("auth") + const userModuleService = container.resolve("user") + + const user = await userModuleService.createUsers({ + email: "admin@medusa.js", + + }) + const authIdentity = await authModuleService.createAuthIdentities({ + provider_identities: [ + { + provider: "emailpass", + entity_id: "admin@medusa.js", + provider_metadata: { + password: "supersecret", + }, + }, + ], + app_metadata: { + user_id: user.id, + }, + }) + + const token = jwt.sign( + { + actor_id: user.id, + actor_type: "user", + auth_identity_id: authIdentity.id, + }, + "supersecret", + { + expiresIn: "1d", + } + ) + + headers["authorization"] = `Bearer ${token}` + }) + it("returns correct message", async () => { + const response = await api.get( + `/admin/custom`, + { headers } + ) + + expect(response.status).toEqual(200) + }) + }) + }) + }, +}) - doSomethingStep(today) - } -) +jest.setTimeout(60 * 1000) ``` -In this workflow, `today` is only evaluated when the workflow is executed. - -*** +### Customer User -## Caveats +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={customerHighlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { ApiKeyDTO } from "@medusajs/framework/types" +import jwt from "jsonwebtoken" +import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows" -### Transform Evaluation +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + describe("GET /custom", () => { + const headers: Record<string, string> = { + } + beforeEach(async () => { + const container = getContainer() + + const authModuleService = container.resolve("auth") + const customerModuleService = container.resolve("customer") + + const customer = await customerModuleService.createCustomers({ + email: "admin@medusa.js", + + }) + const authIdentity = await authModuleService.createAuthIdentities({ + provider_identities: [ + { + provider: "emailpass", + entity_id: "customer@medusa.js", + provider_metadata: { + password: "supersecret", + }, + }, + ], + app_metadata: { + user_id: customer.id, + }, + }) + + const token = jwt.sign( + { + actor_id: customer.id, + actor_type: "customer", + auth_identity_id: authIdentity.id, + }, + "supersecret", + { + expiresIn: "1d", + } + ) + + headers["authorization"] = `Bearer ${token}` -`transform`'s value is only evaluated if you pass its output to a step or in the workflow response. -For example, if you have the following workflow: + const pak = (await createApiKeysWorkflow(getContainer()).run({ + input: { + api_keys: [ + { + type: "publishable", + title: "Test Key", + created_by: "", + }, + ], + }, + })).result[0] -```ts -const myWorkflow = createWorkflow( - "hello-world", - function (input) { - const str = transform( - { input }, - (data) => `${data.input.str1}${data.input.str2}` - ) + headers["x-publishable-api-key"] = pak.token + }) + it("returns correct message", async () => { + const response = await api.get( + `/store/customers/me/custom`, + { headers } + ) + + expect(response.status).toEqual(200) + }) + }) + }) + }, +}) - return new WorkflowResponse("done") - } -) +jest.setTimeout(60 * 1000) ``` -Since `str`'s value isn't used as a step's input or passed to `WorkflowResponse`, its value is never evaluated. +In the test suite, you add a `beforeEach` hook that creates a user or customer, an auth identity, and generates a JWT token for them. The JWT token is then set in the `Authorization` header of the request. -### Data Validation +You also create and pass a publishable API key in the header for the customer as it's required for requests to `/store` routes. Learn more in [this section](#pass-publishable-api-key). -`transform` should only be used to perform variable or data manipulation. +*** -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. +## Upload Files in Test Requests + +If your API route requires uploading a file, create a `FormData` object imported from the `form-data` object, then pass the form data headers in the request. For example: -```ts -// DON'T -const myWorkflow = createWorkflow( - "hello-world", - function (input) { - const str = transform( - { input }, - (data) => { - if (!input.str1) { - throw new Error("Not allowed!") - } - } - ) - } -) +The `form-data` package is available by default. -// DO -const validateHasStr1Step = createStep( - "validate-has-str1", - ({ input }) => { - if (!input.str1) { - throw new Error("Not allowed!") - } - } -) +```ts title="integration-tests/http/custom-routes.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import FormData from "form-data" -const myWorkflow = createWorkflow( - "hello-world", - function (input) { - validateHasStr1({ - input, +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + describe("GET /custom", () => { + it("upload file", async () => { + const form = new FormData() + form.append("files", Buffer.from("content 1"), "image1.jpg") + form.append("files", Buffer.from("content 2"), "image2.jpg") + + const response = await api.post(`/custom`, form, { + headers: form.getHeaders(), + }) + + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("files") + expect(response.data.files).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + url: expect.any(String), + }), + ]) + ) + }) + }) }) + }, +}) - // workflow continues its execution only if - // the step doesn't throw the error. - } -) +jest.setTimeout(60 * 1000) ``` +You don't have to actually upload a file, you use the `form.append` method to append to a `files` field in the form data object, and you pass random content using the `Buffer.from` method. -# Workflow Hooks - -In this chapter, you'll learn what a workflow hook is and how to consume them. +Then, you pass to the `api.post` method the form data object as a second parameter, and an object with the `headers` property set to the form data object's headers as a third parameter. -## What is a Workflow Hook? +If you're passing authentication or other headers, you can pass both the form data headers and the authentication headers in the same object: -A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler. +```ts title="integration-tests/http/custom-routes.spec.ts" +const response = await api.post(`/custom`, form, { + headers: { + ...form.getHeaders(), + ...authHeaders, + }, +}) +``` -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. -Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks. +# Example: Write Integration Tests for Workflows -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 write integration tests for workflows using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framwork. -*** +### Prerequisites -## How to Consume a Hook? +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -A workflow has a special `hooks` property which is an object that holds its hooks. +## Write Integration Test for Workflow -So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory: +Consider you have the following workflow defined at `src/workflows/hello-world.ts`: -- Import the workflow. -- Access its hook using the `hooks` property. -- Pass the hook a step function as a parameter to consume it. +```ts title="src/workflows/hello-world.ts" +import { + createWorkflow, + createStep, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content: +const step1 = createStep("step-1", () => { + return new StepResponse("Hello, World!") +}) -```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights} -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +export const helloWorldWorkflow = createWorkflow( + "hello-world-workflow", + () => { + const message = step1() -createProductsWorkflow.hooks.productsCreated( - async ({ products }, { container }) => { - // TODO perform an action + return new WorkflowResponse(message) } ) ``` -The `productsCreated` hook is available on the workflow's `hooks` property by its name. - -You invoke the hook, passing a step function (the hook handler) as a parameter. - -Now, when a product is created using the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), your hook handler is executed after the product is created. - -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. - -### Hook Handler Parameter +To write a test for this workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content: -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. +```ts title="integration-tests/http/workflow.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { helloWorldWorkflow } from "../../src/workflows/hello-world" -Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product. +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test hello-world workflow", () => { + it("returns message", async () => { + const { result } = await helloWorldWorkflow(getContainer()) + .run() -### Hook Handler Compensation + expect(result).toEqual("Hello, World!") + }) + }) + }, +}) -Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook. +jest.setTimeout(60 * 1000) +``` -For example: +You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`. -```ts title="src/workflows/hooks/product-created.ts" -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +### Jest Timeout -createProductsWorkflow.hooks.productsCreated( - async ({ products }, { container }) => { - // TODO perform an action +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: - return new StepResponse(undefined, { ids }) - }, - async ({ ids }, { container }) => { - // undo the performed action - } -) +```ts title="integration-tests/http/custom-routes.spec.ts" +// in your test's file +jest.setTimeout(60 * 1000) ``` -The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler. - -The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function. - -It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container. - -### Additional Data Property +*** -Medusa's workflows pass in the hook's input an `additional_data` property: +## Run Test -```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]} -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +Run the following command to run your tests: -createProductsWorkflow.hooks.productsCreated( - async ({ products, additional_data }, { container }) => { - // TODO perform an action - } -) +```bash npm2yarn +npm run test:integration ``` -This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow. +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). -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). +This runs your Medusa application and runs the tests available under the `integrations/http` directory. -### 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: +## Test That a Workflow Throws an Error -```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" +You might want to test that a workflow throws an error in certain cases. To test this: -export async function POST(req: MedusaRequest, res: MedusaResponse) { - await createProductsWorkflow(req.scope).run({ - input: { - products: [ - // ... - ], - additional_data: { - custom_field: "test", - }, - }, - }) -} -``` +- Disable the `throwOnError` option when executing the workflow. +- Use the returned `errors` property to check what errors were thrown. -Your hook handler then receives that passed data in the `additional_data` object. +For example, if you have a step that throws this error: +```ts title="src/workflows/hello-world.ts" +import { MedusaError } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" -# Workflow Timeout +const step1 = createStep("step-1", () => { + throw new MedusaError(MedusaError.Types.NOT_FOUND, "Item doesn't exist") +}) +``` -In this chapter, you’ll learn how to set a timeout for workflows and steps. +You can write the following test to ensure that the workflow throws that error: -## What is a Workflow Timeout? +```ts title="integration-tests/http/workflow.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { helloWorldWorkflow } from "../../src/workflows/hello-world" -By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test hello-world workflow", () => { + it("returns message", async () => { + const { errors } = await helloWorldWorkflow(getContainer()) + .run({ + throwOnError: false, + }) -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. + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].error.message).toBe("Item doesn't exist") + }) + }) + }, +}) -### Timeout Doesn't Stop Step Execution +jest.setTimeout(60 * 1000) +``` -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. +The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, being the error thrown. -*** +If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. -## Configure Workflow Timeout -The `createWorkflow` function can accept a configuration object instead of the workflow’s name. +# Example: Integration Tests for a Module -In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. +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. -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" +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -const step1 = createStep( - "step-1", - async () => { - // ... - } -) +## Write Integration Test for Module -const myWorkflow = createWorkflow({ - name: "hello-world", - timeout: 2, // 2 seconds -}, function () { - const str1 = step1() +Consider a `hello` module with a `HelloModuleService` that has a `getMessage` method: - return new WorkflowResponse({ - message: str1, - }) -}) +```ts title="src/modules/hello/service.ts" +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" -export default myWorkflow +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + getMessage(): string { + return "Hello, World!" + } +} +export default HelloModuleService ``` -This workflow's executions fail if they run longer than two seconds. +To create an integration test for the method, create the file `src/modules/hello/__tests__/service.spec.ts` with the following content: -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`. +```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<HelloModuleService>({ + moduleName: HELLO_MODULE, + moduleModels: [MyCustom], + resolve: "./src/modules/hello", + testSuite: ({ service }) => { + describe("HelloModuleService", () => { + it("says hello world", () => { + const message = service.getMessage() -## Configure Step Timeout + expect(message).toEqual("Hello, World!") + }) + }) + }, +}) -Alternatively, you can configure the timeout for a step rather than the entire workflow. +jest.setTimeout(60 * 1000) +``` -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. +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 step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. +*** -For example: +## Run Test -```tsx -const step1 = createStep( - { - name: "step-1", - timeout: 2, // 2 seconds - }, - async () => { - // ... - } -) +Run the following command to run your module integration tests: + +```bash npm2yarn +npm run test:integration:modules ``` -This step's executions fail if they run longer than two seconds. +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). -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`. +This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. # Commerce Modules @@ -14852,24 +14852,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# 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. @@ -14877,54 +14877,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, }) } ) @@ -14934,19 +14926,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) } @@ -14954,19 +14948,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) } @@ -14978,15 +14974,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) } @@ -15132,316 +15130,24 @@ Medusa provides the following authentication providers out-of-the-box. You can u *** -# Customer Module - -In this section of the documentation, you will find resources to learn more about the Customer Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers and groups using the dashboard. - -Medusa has customer related features available out-of-the-box through the Customer 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 Customer Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Customer Features - -- [Customer Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/customer-accounts/index.html.md): Store and manage guest and registered customers in your store. -- [Customer Organization](https://docs.medusajs.com/references/customer/models/index.html.md): Organize customers into groups. This has a lot of benefits and supports many use cases, such as provide discounts for specific customer groups using the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md). - -*** - -## How to Use the Customer 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. - -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-customer.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createCustomerStep = createStep( - "create-customer", - async ({}, { container }) => { - const customerModuleService = container.resolve(Modules.CUSTOMER) - - const customer = await customerModuleService.createCustomers({ - first_name: "Peter", - last_name: "Hayes", - email: "peter.hayes@example.com", - }) - - return new StepResponse({ customer }, customer.id) - }, - async (customerId, { container }) => { - if (!customerId) { - return - } - const customerModuleService = container.resolve(Modules.CUSTOMER) - - await customerModuleService.deleteCustomers([customerId]) - } -) - -export const createCustomerWorkflow = createWorkflow( - "create-customer", - () => { - const { customer } = createCustomerStep() - - return new WorkflowResponse({ - customer, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### 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 { createCustomerWorkflow } from "../../workflows/create-customer" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createCustomerWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### 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 { createCustomerWorkflow } from "../workflows/create-customer" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createCustomerWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createCustomerWorkflow } from "../workflows/create-customer" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createCustomerWorkflow(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). - -*** - - -# 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. - -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). - -## Currency 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. - -*** - -## 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. - -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/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 retrieveCurrencyStep = createStep( - "retrieve-currency", - async ({}, { container }) => { - const currencyModuleService = container.resolve(Modules.CURRENCY) - - const currency = await currencyModuleService - .retrieveCurrency("usd") - - return new StepResponse({ currency }) - } -) - -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({ - formattedPrice, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```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 { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await retrievePriceWithCurrency(req.scope) - .run({ - price: 10, - }) - - 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", -} -``` - -### 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" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await retrievePriceWithCurrency(container) - .run({ - price: 10, - }) - - 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). - -*** - - -# Inventory Module - -In this section of the documentation, you will find resources to learn more about the Inventory Module and how to use it in your application. +# Cart Module -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features using the dashboard. +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 inventory related features available out-of-the-box through the Inventory 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 Inventory 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). -## Inventory Features +## Cart Features -- [Inventory Items Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md): Store and manage inventory of any stock-kept item, such as product variants. -- [Inventory Across Locations](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventorylevel/index.html.md): Manage inventory levels across different locations, such as warehouses. -- [Reservation Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#reservationitem/index.html.md): Reserve quantities of inventory items at specific locations for orders or other purposes. -- [Check Inventory Availability](https://docs.medusajs.com/references/inventory-next/confirmInventory/index.html.md): Check whether an inventory item has the necessary quantity for purchase. -- [Inventory Kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. +- [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 Inventory 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. @@ -15449,7 +15155,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-inventory-item.ts" highlights={highlights} +```ts title="src/workflows/create-cart.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -15458,36 +15164,45 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createInventoryItemStep = createStep( - "create-inventory-item", +const createCartStep = createStep( + "create-cart", async ({}, { container }) => { - const inventoryModuleService = container.resolve(Modules.INVENTORY) + const cartModuleService = container.resolve(Modules.CART) - const inventoryItem = await inventoryModuleService.createInventoryItems({ - sku: "SHIRT", - title: "Green Medusa Shirt", - requires_shipping: true, + 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({ inventoryItem }, inventoryItem.id) + return new StepResponse({ cart }, cart.id) }, - async (inventoryItemId, { container }) => { - if (!inventoryItemId) { + async (cartId, { container }) => { + if (!cartId) { return } - const inventoryModuleService = container.resolve(Modules.INVENTORY) + const cartModuleService = container.resolve(Modules.CART) - await inventoryModuleService.deleteInventoryItems([inventoryItemId]) + await cartModuleService.deleteCarts([cartId]) } ) -export const createInventoryItemWorkflow = createWorkflow( - "create-inventory-item-workflow", +export const createCartWorkflow = createWorkflow( + "create-cart", () => { - const { inventoryItem } = createInventoryItemStep() + const { cart } = createCartStep() return new WorkflowResponse({ - inventoryItem, + cart, }) } ) @@ -15502,13 +15217,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" +import { createCartWorkflow } from "../../workflows/create-cart" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createInventoryItemWorkflow(req.scope) + const { result } = await createCartWorkflow(req.scope) .run() res.send(result) @@ -15522,13 +15237,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" +import { createCartWorkflow } from "../workflows/create-cart" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createInventoryItemWorkflow(container) + const { result } = await createCartWorkflow(container) .run() console.log(result) @@ -15543,12 +15258,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" +import { createCartWorkflow } from "../workflows/create-cart" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createInventoryItemWorkflow(container) + const { result } = await createCartWorkflow(container) .run() console.log(result) @@ -15731,27 +15446,27 @@ The Fulfillment Module accepts options for further configurations. Refer to [thi *** -# Order Module +# Inventory Module -In this section of the documentation, you will find resources to learn more about the Order Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Inventory Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/index.html.md) to learn how to manage orders using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features using the dashboard. -Medusa has order related features available out-of-the-box through the Order 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 Order Module. +Medusa has inventory related features available out-of-the-box through the Inventory 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 Inventory Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Order Features +## Inventory Features -- [Order Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/concepts/index.html.md): Store and manage your orders to retrieve, create, cancel, and perform other operations. -- Draft Orders: Allow merchants to create orders on behalf of their customers as draft orders that later are transformed to regular orders. -- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/promotion-adjustments/index.html.md): Apply promotions or discounts to the order's 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/order/tax-lines/index.html.md): Apply tax lines to an order's line items and shipping methods. -- [Returns](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md), [Edits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md), [Exchanges](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md), and [Claims](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md): Make [changes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to an order to edit, return, or exchange its items, with [version-based control](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) over the order's timeline. +- [Inventory Items Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md): Store and manage inventory of any stock-kept item, such as product variants. +- [Inventory Across Locations](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventorylevel/index.html.md): Manage inventory levels across different locations, such as warehouses. +- [Reservation Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#reservationitem/index.html.md): Reserve quantities of inventory items at specific locations for orders or other purposes. +- [Check Inventory Availability](https://docs.medusajs.com/references/inventory-next/confirmInventory/index.html.md): Check whether an inventory item has the necessary quantity for purchase. +- [Inventory Kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. *** -## How to Use the Order Module +## How to Use the Inventory 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. @@ -15759,7 +15474,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-draft-order.ts" highlights={highlights} +```ts title="src/workflows/create-inventory-item.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -15768,48 +15483,36 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createDraftOrderStep = createStep( - "create-order", +const createInventoryItemStep = createStep( + "create-inventory-item", async ({}, { container }) => { - const orderModuleService = container.resolve(Modules.ORDER) + const inventoryModuleService = container.resolve(Modules.INVENTORY) - const draftOrder = await orderModuleService.createOrders({ - currency_code: "usd", - items: [ - { - title: "Shirt", - quantity: 1, - unit_price: 3000, - }, - ], - shipping_methods: [ - { - name: "Express shipping", - amount: 3000, - }, - ], - status: "draft", + const inventoryItem = await inventoryModuleService.createInventoryItems({ + sku: "SHIRT", + title: "Green Medusa Shirt", + requires_shipping: true, }) - return new StepResponse({ draftOrder }, draftOrder.id) + return new StepResponse({ inventoryItem }, inventoryItem.id) }, - async (draftOrderId, { container }) => { - if (!draftOrderId) { + async (inventoryItemId, { container }) => { + if (!inventoryItemId) { return } - const orderModuleService = container.resolve(Modules.ORDER) + const inventoryModuleService = container.resolve(Modules.INVENTORY) - await orderModuleService.deleteOrders([draftOrderId]) + await inventoryModuleService.deleteInventoryItems([inventoryItemId]) } ) -export const createDraftOrderWorkflow = createWorkflow( - "create-draft-order", +export const createInventoryItemWorkflow = createWorkflow( + "create-inventory-item-workflow", () => { - const { draftOrder } = createDraftOrderStep() + const { inventoryItem } = createInventoryItemStep() return new WorkflowResponse({ - draftOrder, + inventoryItem, }) } ) @@ -15824,13 +15527,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createDraftOrderWorkflow } from "../../workflows/create-draft-order" +import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createDraftOrderWorkflow(req.scope) + const { result } = await createInventoryItemWorkflow(req.scope) .run() res.send(result) @@ -15844,13 +15547,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createDraftOrderWorkflow } from "../workflows/create-draft-order" +import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createDraftOrderWorkflow(container) + const { result } = await createInventoryItemWorkflow(container) .run() console.log(result) @@ -15865,12 +15568,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createDraftOrderWorkflow } from "../workflows/create-draft-order" +import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createDraftOrderWorkflow(container) + const { result } = await createInventoryItemWorkflow(container) .run() console.log(result) @@ -15887,27 +15590,27 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Payment Module +# Order Module -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. +In this section of the documentation, you will find resources to learn more about the Order 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. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/index.html.md) to learn how to manage orders using the dashboard. -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 has order related features available out-of-the-box through the Order 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 Order 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 +## Order 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. +- [Order Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/concepts/index.html.md): Store and manage your orders to retrieve, create, cancel, and perform other operations. +- Draft Orders: Allow merchants to create orders on behalf of their customers as draft orders that later are transformed to regular orders. +- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/promotion-adjustments/index.html.md): Apply promotions or discounts to the order's 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/order/tax-lines/index.html.md): Apply tax lines to an order's line items and shipping methods. +- [Returns](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md), [Edits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md), [Exchanges](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md), and [Claims](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md): Make [changes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to an order to edit, return, or exchange its items, with [version-based control](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) over the order's timeline. *** -## How to Use the Payment Module +## How to Use the Order 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. @@ -15915,7 +15618,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-draft-order.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -15924,35 +15627,48 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createPaymentCollectionStep = createStep( - "create-payment-collection", +const createDraftOrderStep = createStep( + "create-order", async ({}, { container }) => { - const paymentModuleService = container.resolve(Modules.PAYMENT) + const orderModuleService = container.resolve(Modules.ORDER) - const paymentCollection = await paymentModuleService.createPaymentCollections({ + const draftOrder = await orderModuleService.createOrders({ currency_code: "usd", - amount: 5000, + items: [ + { + title: "Shirt", + quantity: 1, + unit_price: 3000, + }, + ], + shipping_methods: [ + { + name: "Express shipping", + amount: 3000, + }, + ], + status: "draft", }) - return new StepResponse({ paymentCollection }, paymentCollection.id) + return new StepResponse({ draftOrder }, draftOrder.id) }, - async (paymentCollectionId, { container }) => { - if (!paymentCollectionId) { + async (draftOrderId, { container }) => { + if (!draftOrderId) { return } - const paymentModuleService = container.resolve(Modules.PAYMENT) + const orderModuleService = container.resolve(Modules.ORDER) - await paymentModuleService.deletePaymentCollections([paymentCollectionId]) + await orderModuleService.deleteOrders([draftOrderId]) } ) -export const createPaymentCollectionWorkflow = createWorkflow( - "create-payment-collection", +export const createDraftOrderWorkflow = createWorkflow( + "create-draft-order", () => { - const { paymentCollection } = createPaymentCollectionStep() + const { draftOrder } = createDraftOrderStep() return new WorkflowResponse({ - paymentCollection, + draftOrder, }) } ) @@ -15967,13 +15683,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection" +import { createDraftOrderWorkflow } from "../../workflows/create-draft-order" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createPaymentCollectionWorkflow(req.scope) + const { result } = await createDraftOrderWorkflow(req.scope) .run() res.send(result) @@ -15987,13 +15703,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" +import { createDraftOrderWorkflow } from "../workflows/create-draft-order" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createPaymentCollectionWorkflow(container) + const { result } = await createDraftOrderWorkflow(container) .run() console.log(result) @@ -16008,12 +15724,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 { createDraftOrderWorkflow } from "../workflows/create-draft-order" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createPaymentCollectionWorkflow(container) + const { result } = await createDraftOrderWorkflow(container) .run() console.log(result) @@ -16029,18 +15745,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. - -*** - # Pricing Module @@ -16196,25 +15900,27 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Product Module +# Payment Module -In this section of the documentation, you will find resources to learn more about the Product 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/products/index.html.md) to learn how to manage products 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 product related features available out-of-the-box through the Product 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 Product 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). -## Product Features +## Payment Features -- [Products Management](https://docs.medusajs.com/references/product/models/Product/index.html.md): Store and manage products. Products have custom options, such as color or size, and each variant in the product sets the value for these options. -- [Product Organization](https://docs.medusajs.com/references/product/models/index.html.md): The Product Module provides different data models used to organize products, including categories, collections, tags, and more. -- [Bundled and Multi-Part Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. +- [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 Product 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. @@ -16222,7 +15928,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-product.ts" highlights={highlights} +```ts title="src/workflows/create-payment-collection.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -16231,48 +15937,35 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createProductStep = createStep( - "create-product", +const createPaymentCollectionStep = createStep( + "create-payment-collection", async ({}, { container }) => { - const productService = container.resolve(Modules.PRODUCT) + const paymentModuleService = container.resolve(Modules.PAYMENT) - const product = await productService.createProducts({ - title: "Medusa Shirt", - options: [ - { - title: "Color", - values: ["Black", "White"], - }, - ], - variants: [ - { - title: "Black Shirt", - options: { - Color: "Black", - }, - }, - ], + const paymentCollection = await paymentModuleService.createPaymentCollections({ + currency_code: "usd", + amount: 5000, }) - return new StepResponse({ product }, product.id) + return new StepResponse({ paymentCollection }, paymentCollection.id) }, - async (productId, { container }) => { - if (!productId) { + async (paymentCollectionId, { container }) => { + if (!paymentCollectionId) { return } - const productService = container.resolve(Modules.PRODUCT) + const paymentModuleService = container.resolve(Modules.PAYMENT) - await productService.deleteProducts([productId]) + await paymentModuleService.deletePaymentCollections([paymentCollectionId]) } ) -export const createProductWorkflow = createWorkflow( - "create-product", +export const createPaymentCollectionWorkflow = createWorkflow( + "create-payment-collection", () => { - const { product } = createProductStep() + const { paymentCollection } = createPaymentCollectionStep() return new WorkflowResponse({ - product, + paymentCollection, }) } ) @@ -16287,13 +15980,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createProductWorkflow } from "../../workflows/create-product" +import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createProductWorkflow(req.scope) + const { result } = await createPaymentCollectionWorkflow(req.scope) .run() res.send(result) @@ -16307,13 +16000,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createProductWorkflow } from "../workflows/create-product" +import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createProductWorkflow(container) + const { result } = await createPaymentCollectionWorkflow(container) .run() console.log(result) @@ -16328,12 +16021,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createProductWorkflow } from "../workflows/create-product" +import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createProductWorkflow(container) + const { result } = await createPaymentCollectionWorkflow(container) .run() console.log(result) @@ -16349,182 +16042,38 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +## Configure Payment Module -# Region Module - -In this section of the documentation, you will find resources to learn more about the Region Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage regions using the dashboard. - -Medusa has region related features available out-of-the-box through the Region 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 Region Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -*** - -## Region Features - -- [Region Management](https://docs.medusajs.com/references/region/models/Region/index.html.md): Manage regions in your store. You can create regions with different currencies and settings. -- [Multi-Currency Support](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has a currency. You can support multiple currencies in your store by creating multiple regions. -- [Different Settings Per Region](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has its own settings, such as what countries belong to a region or its tax settings. +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. *** -## How to Use Region 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-region.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createRegionStep = createStep( - "create-region", - async ({}, { container }) => { - const regionModuleService = container.resolve(Modules.REGION) - - const region = await regionModuleService.createRegions({ - name: "Europe", - currency_code: "eur", - }) - - return new StepResponse({ region }, region.id) - }, - async (regionId, { container }) => { - if (!regionId) { - return - } - const regionModuleService = container.resolve(Modules.REGION) - - await regionModuleService.deleteRegions([regionId]) - } -) - -export const createRegionWorkflow = createWorkflow( - "create-region", - () => { - const { region } = createRegionStep() - - return new WorkflowResponse({ - region, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### 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 { createRegionWorkflow } from "../../workflows/create-region" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createRegionWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### 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 { createRegionWorkflow } from "../workflows/create-region" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createRegionWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createRegionWorkflow } from "../workflows/create-region" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createRegionWorkflow(container) - .run() - - 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 payment providers out-of-the-box. You can use them to process payments for orders, returns, and other resources. *** -# Sales Channel Module +# Product 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 Product 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/products/index.html.md) to learn how to manage products 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 product related features available out-of-the-box through the Product 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 Product 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 +## Product 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. +- [Products Management](https://docs.medusajs.com/references/product/models/Product/index.html.md): Store and manage products. Products have custom options, such as color or size, and each variant in the product sets the value for these options. +- [Product Organization](https://docs.medusajs.com/references/product/models/index.html.md): The Product Module provides different data models used to organize products, including categories, collections, tags, and more. +- [Bundled and Multi-Part Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. *** -## How to Use Sales Channel Module's Service +## How to Use the Product 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. @@ -16532,7 +16081,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-product.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -16541,41 +16090,48 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createSalesChannelStep = createStep( - "create-sales-channel", +const createProductStep = createStep( + "create-product", async ({}, { container }) => { - const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) + const productService = container.resolve(Modules.PRODUCT) - const salesChannels = await salesChannelModuleService.createSalesChannels([ - { - name: "B2B", - }, - { - name: "Mobile App", - }, - ]) + const product = await productService.createProducts({ + title: "Medusa Shirt", + options: [ + { + title: "Color", + values: ["Black", "White"], + }, + ], + variants: [ + { + title: "Black Shirt", + options: { + Color: "Black", + }, + }, + ], + }) - return new StepResponse({ salesChannels }, salesChannels.map((sc) => sc.id)) + return new StepResponse({ product }, product.id) }, - async (salesChannelIds, { container }) => { - if (!salesChannelIds) { + async (productId, { container }) => { + if (!productId) { return } - const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) + const productService = container.resolve(Modules.PRODUCT) - await salesChannelModuleService.deleteSalesChannels( - salesChannelIds - ) + await productService.deleteProducts([productId]) } ) -export const createSalesChannelWorkflow = createWorkflow( - "create-sales-channel", +export const createProductWorkflow = createWorkflow( + "create-product", () => { - const { salesChannels } = createSalesChannelStep() + const { product } = createProductStep() return new WorkflowResponse({ - salesChannels, + product, }) } ) @@ -16590,13 +16146,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createSalesChannelWorkflow } from "../../workflows/create-sales-channel" +import { createProductWorkflow } from "../../workflows/create-product" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createSalesChannelWorkflow(req.scope) + const { result } = await createProductWorkflow(req.scope) .run() res.send(result) @@ -16610,13 +16166,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createSalesChannelWorkflow } from "../workflows/create-sales-channel" +import { createProductWorkflow } from "../workflows/create-product" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createSalesChannelWorkflow(container) + const { result } = await createProductWorkflow(container) .run() console.log(result) @@ -16631,12 +16187,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 { createProductWorkflow } from "../workflows/create-product" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createSalesChannelWorkflow(container) + const { result } = await createProductWorkflow(container) .run() console.log(result) @@ -16653,24 +16209,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Stock Location Module +# Customer 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 Customer 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/customers/index.html.md) to learn how to manage customers and groups 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 customer related features available out-of-the-box through the Customer 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 Customer 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 +## Customer Features -- [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. +- [Customer Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/customer-accounts/index.html.md): Store and manage guest and registered customers in your store. +- [Customer Organization](https://docs.medusajs.com/references/customer/models/index.html.md): Organize customers into groups. This has a lot of benefits and supports many use cases, such as provide discounts for specific customer groups using the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md). *** -## How to Use Stock Location Module's Service +## How to Use the Customer 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. @@ -16678,7 +16234,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-customer.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -16687,33 +16243,37 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createStockLocationStep = createStep( - "create-stock-location", +const createCustomerStep = createStep( + "create-customer", async ({}, { container }) => { - const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + const customerModuleService = container.resolve(Modules.CUSTOMER) - const stockLocation = await stockLocationModuleService.createStockLocations({ - name: "Warehouse 1", + const customer = await customerModuleService.createCustomers({ + first_name: "Peter", + last_name: "Hayes", + email: "peter.hayes@example.com", }) - return new StepResponse({ stockLocation }, stockLocation.id) + return new StepResponse({ customer }, customer.id) }, - async (stockLocationId, { container }) => { - if (!stockLocationId) { + async (customerId, { container }) => { + if (!customerId) { return } - const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + const customerModuleService = container.resolve(Modules.CUSTOMER) - await stockLocationModuleService.deleteStockLocations([stockLocationId]) + await customerModuleService.deleteCustomers([customerId]) } ) -export const createStockLocationWorkflow = createWorkflow( - "create-stock-location", +export const createCustomerWorkflow = createWorkflow( + "create-customer", () => { - const { stockLocation } = createStockLocationStep() + const { customer } = createCustomerStep() - return new WorkflowResponse({ stockLocation }) + return new WorkflowResponse({ + customer, + }) } ) ``` @@ -16727,13 +16287,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createStockLocationWorkflow } from "../../workflows/create-stock-location" +import { createCustomerWorkflow } from "../../workflows/create-customer" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createStockLocationWorkflow(req.scope) + const { result } = await createCustomerWorkflow(req.scope) .run() res.send(result) @@ -16747,13 +16307,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createStockLocationWorkflow } from "../workflows/create-stock-location" +import { createCustomerWorkflow } from "../workflows/create-customer" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createStockLocationWorkflow(container) + const { result } = await createCustomerWorkflow(container) .run() console.log(result) @@ -16768,12 +16328,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 { createCustomerWorkflow } from "../workflows/create-customer" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createStockLocationWorkflow(container) + const { result } = await createCustomerWorkflow(container) .run() console.log(result) @@ -16790,26 +16350,27 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Promotion Module +# Region Module -In this section of the documentation, you will find resources to learn more about the Promotion Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Region Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage regions using the dashboard. -Medusa has promotion related features available out-of-the-box through the Promotion 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 Promotion Module. +Medusa has region related features available out-of-the-box through the Region 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 Region Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Promotion Features +*** -- [Discount Functionalities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts/index.html.md): A promotion discounts an amount or percentage of a cart's items, shipping methods, or the entire order. -- [Flexible Promotion Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts#flexible-rules/index.html.md): A promotion has rules that restricts when the promotion is applied. -- [Campaign Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/campaign/index.html.md): A campaign combines promotions under the same conditions, such as start and end dates, and budget configurations. -- [Apply Promotion on Carts and Orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md): Apply promotions on carts and orders to discount items, shipping methods, or the entire order. +## Region Features + +- [Region Management](https://docs.medusajs.com/references/region/models/Region/index.html.md): Manage regions in your store. You can create regions with different currencies and settings. +- [Multi-Currency Support](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has a currency. You can support multiple currencies in your store by creating multiple regions. +- [Different Settings Per Region](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has its own settings, such as what countries belong to a region or its tax settings. *** -## How to Use the Promotion Module +## How to Use Region 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. @@ -16817,7 +16378,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-promotion.ts" highlights={highlights} +```ts title="src/workflows/create-region.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -16826,41 +16387,35 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createPromotionStep = createStep( - "create-promotion", +const createRegionStep = createStep( + "create-region", async ({}, { container }) => { - const promotionModuleService = container.resolve(Modules.PROMOTION) + const regionModuleService = container.resolve(Modules.REGION) - const promotion = await promotionModuleService.createPromotions({ - code: "10%OFF", - type: "standard", - application_method: { - type: "percentage", - target_type: "order", - value: 10, - currency_code: "usd", - }, + const region = await regionModuleService.createRegions({ + name: "Europe", + currency_code: "eur", }) - return new StepResponse({ promotion }, promotion.id) + return new StepResponse({ region }, region.id) }, - async (promotionId, { container }) => { - if (!promotionId) { + async (regionId, { container }) => { + if (!regionId) { return } - const promotionModuleService = container.resolve(Modules.PROMOTION) + const regionModuleService = container.resolve(Modules.REGION) - await promotionModuleService.deletePromotions(promotionId) + await regionModuleService.deleteRegions([regionId]) } ) -export const createPromotionWorkflow = createWorkflow( - "create-promotion", +export const createRegionWorkflow = createWorkflow( + "create-region", () => { - const { promotion } = createPromotionStep() + const { region } = createRegionStep() return new WorkflowResponse({ - promotion, + region, }) } ) @@ -16875,13 +16430,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createPromotionWorkflow } from "../../workflows/create-cart" +import { createRegionWorkflow } from "../../workflows/create-region" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createPromotionWorkflow(req.scope) + const { result } = await createRegionWorkflow(req.scope) .run() res.send(result) @@ -16895,13 +16450,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createPromotionWorkflow } from "../workflows/create-cart" +import { createRegionWorkflow } from "../workflows/create-region" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createPromotionWorkflow(container) + const { result } = await createRegionWorkflow(container) .run() console.log(result) @@ -16916,12 +16471,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createPromotionWorkflow } from "../workflows/create-cart" +import { createRegionWorkflow } from "../workflows/create-region" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createPromotionWorkflow(container) + const { result } = await createRegionWorkflow(container) .run() console.log(result) @@ -16938,24 +16493,26 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Store Module +# Promotion Module -In this section of the documentation, you will find resources to learn more about the Store Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Promotion Module and how to use it in your application. -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 using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. -Medusa has store related features available out-of-the-box through the Store 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 Store Module. +Medusa has promotion related features available out-of-the-box through the Promotion 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 Promotion Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Store Features +## Promotion Features -- [Store Management](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create and manage stores in your application. -- [Multi-Tenancy Support](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create multiple stores, each having its own configurations. +- [Discount Functionalities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts/index.html.md): A promotion discounts an amount or percentage of a cart's items, shipping methods, or the entire order. +- [Flexible Promotion Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts#flexible-rules/index.html.md): A promotion has rules that restricts when the promotion is applied. +- [Campaign Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/campaign/index.html.md): A campaign combines promotions under the same conditions, such as start and end dates, and budget configurations. +- [Apply Promotion on Carts and Orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md): Apply promotions on carts and orders to discount items, shipping methods, or the entire order. *** -## How to Use Store Module's Service +## How to Use the Promotion 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. @@ -16963,7 +16520,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-store.ts" highlights={highlights} +```ts title="src/workflows/create-promotion.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -16972,37 +16529,42 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createStoreStep = createStep( - "create-store", +const createPromotionStep = createStep( + "create-promotion", async ({}, { container }) => { - const storeModuleService = container.resolve(Modules.STORE) + const promotionModuleService = container.resolve(Modules.PROMOTION) - const store = await storeModuleService.createStores({ - name: "My Store", - supported_currencies: [{ + const promotion = await promotionModuleService.createPromotions({ + code: "10%OFF", + type: "standard", + application_method: { + type: "percentage", + target_type: "order", + value: 10, currency_code: "usd", - is_default: true, - }], + }, }) - return new StepResponse({ store }, store.id) + return new StepResponse({ promotion }, promotion.id) }, - async (storeId, { container }) => { - if(!storeId) { + async (promotionId, { container }) => { + if (!promotionId) { return } - const storeModuleService = container.resolve(Modules.STORE) - - await storeModuleService.deleteStores([storeId]) + const promotionModuleService = container.resolve(Modules.PROMOTION) + + await promotionModuleService.deletePromotions(promotionId) } ) -export const createStoreWorkflow = createWorkflow( - "create-store", +export const createPromotionWorkflow = createWorkflow( + "create-promotion", () => { - const { store } = createStoreStep() + const { promotion } = createPromotionStep() - return new WorkflowResponse({ store }) + return new WorkflowResponse({ + promotion, + }) } ) ``` @@ -17016,13 +16578,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createStoreWorkflow } from "../../workflows/create-store" +import { createPromotionWorkflow } from "../../workflows/create-cart" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createStoreWorkflow(req.scope) + const { result } = await createPromotionWorkflow(req.scope) .run() res.send(result) @@ -17036,13 +16598,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createStoreWorkflow } from "../workflows/create-store" +import { createPromotionWorkflow } from "../workflows/create-cart" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createStoreWorkflow(container) + const { result } = await createPromotionWorkflow(container) .run() console.log(result) @@ -17057,12 +16619,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createStoreWorkflow } from "../workflows/create-store" +import { createPromotionWorkflow } from "../workflows/create-cart" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createStoreWorkflow(container) + const { result } = await createPromotionWorkflow(container) .run() console.log(result) @@ -17079,24 +16641,38 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# User Module +# Sales Channel Module -In this section of the documentation, you will find resources to learn more about the User 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/users/index.html.md) to learn how to manage users 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 user related features available out-of-the-box through the User 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 User 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). -## User Features +## What's a Sales Channel? -- [User Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows/index.html.md): Store and manage users in your store. -- [Invite Users](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows#invite-users/index.html.md): Invite users to join your store and manage those invites. +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 User 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. @@ -17104,7 +16680,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-user.ts" highlights={highlights} +```ts title="src/workflows/create-sales-channel.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -17113,36 +16689,41 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createUserStep = createStep( - "create-user", +const createSalesChannelStep = createStep( + "create-sales-channel", async ({}, { container }) => { - const userModuleService = container.resolve(Modules.USER) + const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) - const user = await userModuleService.createUsers({ - email: "user@example.com", - first_name: "John", - last_name: "Smith", - }) + const salesChannels = await salesChannelModuleService.createSalesChannels([ + { + name: "B2B", + }, + { + name: "Mobile App", + }, + ]) - return new StepResponse({ user }, user.id) + return new StepResponse({ salesChannels }, salesChannels.map((sc) => sc.id)) }, - async (userId, { container }) => { - if (!userId) { + async (salesChannelIds, { container }) => { + if (!salesChannelIds) { return } - const userModuleService = container.resolve(Modules.USER) + const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) - await userModuleService.deleteUsers([userId]) + await salesChannelModuleService.deleteSalesChannels( + salesChannelIds + ) } ) -export const createUserWorkflow = createWorkflow( - "create-user", +export const createSalesChannelWorkflow = createWorkflow( + "create-sales-channel", () => { - const { user } = createUserStep() + const { salesChannels } = createSalesChannelStep() return new WorkflowResponse({ - user, + salesChannels, }) } ) @@ -17157,13 +16738,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createUserWorkflow } from "../../workflows/create-user" +import { createSalesChannelWorkflow } from "../../workflows/create-sales-channel" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createUserWorkflow(req.scope) + const { result } = await createSalesChannelWorkflow(req.scope) .run() res.send(result) @@ -17177,13 +16758,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createUserWorkflow } from "../workflows/create-user" +import { createSalesChannelWorkflow } from "../workflows/create-sales-channel" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createUserWorkflow(container) + const { result } = await createSalesChannelWorkflow(container) .run() console.log(result) @@ -17198,12 +16779,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createUserWorkflow } from "../workflows/create-user" +import { createSalesChannelWorkflow } from "../workflows/create-sales-channel" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createUserWorkflow(container) + const { result } = await createSalesChannelWorkflow(container) .run() console.log(result) @@ -17219,32 +16800,25 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -## Configure User Module - -The User Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/module-options/index.html.md) for details on the module's options. - -*** - -# Tax Module +# Stock Location 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. +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/tax-regions/index.html.md) to learn how to manage tax regions 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 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. +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). -## Tax Features +## Stock Location 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. +- [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 Tax 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. @@ -17252,7 +16826,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-tax-region.ts" highlights={highlights} +```ts title="src/workflows/create-stock-location.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -17261,33 +16835,33 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createTaxRegionStep = createStep( - "create-tax-region", +const createStockLocationStep = createStep( + "create-stock-location", async ({}, { container }) => { - const taxModuleService = container.resolve(Modules.TAX) + const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) - const taxRegion = await taxModuleService.createTaxRegions({ - country_code: "us", + const stockLocation = await stockLocationModuleService.createStockLocations({ + name: "Warehouse 1", }) - return new StepResponse({ taxRegion }, taxRegion.id) + return new StepResponse({ stockLocation }, stockLocation.id) }, - async (taxRegionId, { container }) => { - if (!taxRegionId) { + async (stockLocationId, { container }) => { + if (!stockLocationId) { return } - const taxModuleService = container.resolve(Modules.TAX) + const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) - await taxModuleService.deleteTaxRegions([taxRegionId]) + await stockLocationModuleService.deleteStockLocations([stockLocationId]) } ) -export const createTaxRegionWorkflow = createWorkflow( - "create-tax-region", +export const createStockLocationWorkflow = createWorkflow( + "create-stock-location", () => { - const { taxRegion } = createTaxRegionStep() + const { stockLocation } = createStockLocationStep() - return new WorkflowResponse({ taxRegion }) + return new WorkflowResponse({ stockLocation }) } ) ``` @@ -17301,13 +16875,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createTaxRegionWorkflow } from "../../workflows/create-tax-region" +import { createStockLocationWorkflow } from "../../workflows/create-stock-location" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createTaxRegionWorkflow(req.scope) + const { result } = await createStockLocationWorkflow(req.scope) .run() res.send(result) @@ -17321,13 +16895,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createTaxRegionWorkflow } from "../workflows/create-tax-region" +import { createStockLocationWorkflow } from "../workflows/create-stock-location" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createTaxRegionWorkflow(container) + const { result } = await createStockLocationWorkflow(container) .run() console.log(result) @@ -17342,12 +16916,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createTaxRegionWorkflow } from "../workflows/create-tax-region" +import { createStockLocationWorkflow } from "../workflows/create-stock-location" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createTaxRegionWorkflow(container) + const { result } = await createStockLocationWorkflow(container) .run() console.log(result) @@ -17359,594 +16933,480 @@ export const config = { } ``` -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - -## Configure Tax Module - -The Tax Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) for details on the module's options. - -*** - - -# 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: - -- `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. - -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). - -*** - -## API Key Expiration - -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: - -- [`ApiKey` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). - -*** - -## 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. - - - -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", - }, -}) -``` - - -# Cart Concepts - -In this document, you’ll get an overview of the main concepts of a cart. - -## Shipping and Billing Addresses - -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). - - - -*** - -## Line Items - -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. - -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). - -*** - -## 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. - -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. - -### 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. - - -# Promotions Adjustments in Carts - -In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -## What are Adjustment Lines? +*** -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. -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. +# User Module - +In this section of the documentation, you will find resources to learn more about the User Module and how to use it in your application. -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. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users using the dashboard. -*** +Medusa has user related features available out-of-the-box through the User 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 User Module. -## Discountable Option +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -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. +## User Features -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. +- [User Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows/index.html.md): Store and manage users in your store. +- [Invite Users](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows#invite-users/index.html.md): Invite users to join your store and manage those invites. *** -## Promotion Actions +## How to Use User Module's Service -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. +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. -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). +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 collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - ComputeActionAdjustmentLine, - ComputeActionItemLine, - ComputeActionShippingLine, - // ... -} from "@medusajs/framework/types" +```ts title="src/workflows/create-user.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.adjustments", - "shipping_methods.adjustments", - ], -}) +const createUserStep = createStep( + "create-user", + async ({}, { container }) => { + const userModuleService = container.resolve(Modules.USER) -// 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, + const user = await userModuleService.createUsers({ + email: "user@example.com", + first_name: "John", + last_name: "Smith", }) - } -}) -// 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, - }) + return new StepResponse({ user }, user.id) + }, + async (userId, { container }) => { + if (!userId) { + return + } + const userModuleService = container.resolve(Modules.USER) + + await userModuleService.deleteUsers([userId]) } -}) +) -// compute actions -const actions = await promotionModuleService.computeActions( - ["promo_123"], - { - items: lineItemAdjustments, - shipping_methods: shippingMethodAdjustments, +export const createUserWorkflow = createWorkflow( + "create-user", + () => { + const { user } = createUserStep() + + return new WorkflowResponse({ + user, + }) } ) ``` -The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments. +### API Route -```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +```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 { createUserWorkflow } from "../../workflows/create-user" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createUserWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" import { - AddItemAdjustmentAction, - AddShippingMethodAdjustment, - // ... -} from "@medusajs/framework/types" + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createUserWorkflow } from "../workflows/create-user" -// ... +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createUserWorkflow(container) + .run() -await cartModuleService.setLineItemAdjustments( - cart.id, - actions.filter( - (action) => action.action === "addItemAdjustment" - ) as AddItemAdjustmentAction[] -) + console.log(result) +} -await cartModuleService.setShippingMethodAdjustments( - cart.id, - actions.filter( - (action) => - action.action === "addShippingMethodAdjustment" - ) as AddShippingMethodAdjustment[] -) +export const config: SubscriberConfig = { + event: "user.created", +} ``` +### Scheduled Job -# Links between Cart Module and Other Modules - -This document showcases the module links defined between the Cart Module and other commerce modules. +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createUserWorkflow } from "../workflows/create-user" -## Summary +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createUserWorkflow(container) + .run() -The Cart Module has the following links to other modules: + console.log(result) +} -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` -- [`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). +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). *** -## 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. +## Configure User Module -### Retrieve with Query +The User Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/module-options/index.html.md) for details on the module's options. -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 -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "customer.*", - ], -}) +# Tax Module -// carts.order -``` +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. -### useQueryGraphStep +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. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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). -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "customer.*", - ], -}) +## Tax Features -// carts.order -``` +- [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. *** -## Order Module +## How to Use Tax Module's Service -The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management features. +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. -Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. +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: -### Retrieve with Query +```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" -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`: +const createTaxRegionStep = createStep( + "create-tax-region", + async ({}, { container }) => { + const taxModuleService = container.resolve(Modules.TAX) -### query.graph + const taxRegion = await taxModuleService.createTaxRegions({ + country_code: "us", + }) -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "order.*", - ], -}) + return new StepResponse({ taxRegion }, taxRegion.id) + }, + async (taxRegionId, { container }) => { + if (!taxRegionId) { + return + } + const taxModuleService = container.resolve(Modules.TAX) -// carts.order + await taxModuleService.deleteTaxRegions([taxRegionId]) + } +) + +export const createTaxRegionWorkflow = createWorkflow( + "create-tax-region", + () => { + const { taxRegion } = createTaxRegionStep() + + return new WorkflowResponse({ taxRegion }) + } +) ``` -### useQueryGraphStep +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +### 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 { createTaxRegionWorkflow } from "../../workflows/create-tax-region" -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "order.*", - ], -}) +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createTaxRegionWorkflow(req.scope) + .run() -// carts.order + res.send(result) +} ``` -### Manage with Link - -To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +### Subscriber -### link.create +```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 { createTaxRegionWorkflow } from "../workflows/create-tax-region" -```ts -import { Modules } from "@medusajs/framework/utils" +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createTaxRegionWorkflow(container) + .run() -// ... + console.log(result) +} -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.ORDER]: { - order_id: "order_123", - }, -}) +export const config: SubscriberConfig = { + event: "user.created", +} ``` -### createRemoteLinkStep +### Scheduled Job -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createTaxRegionWorkflow } from "../workflows/create-tax-region" -// ... +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createTaxRegionWorkflow(container) + .run() -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.ORDER]: { - order_id: "order_123", - }, -}) + 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). -## Payment Module +*** -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. +## Configure Tax Module -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. +The Tax Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) for details on the module's options. - +*** -### 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`: +# Store Module -### query.graph +In this section of the documentation, you will find resources to learn more about the Store Module and how to use it in your application. -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "payment_collection.*", - ], -}) +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 using the dashboard. -// carts.payment_collection -``` +Medusa has store related features available out-of-the-box through the Store 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 Store Module. -### useQueryGraphStep +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +## Store Features -// ... +- [Store Management](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create and manage stores in your application. +- [Multi-Tenancy Support](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create multiple stores, each having its own configurations. -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "payment_collection.*", - ], -}) +*** -// carts.payment_collection -``` +## How to Use Store Module's Service -### Manage with Link +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. -To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +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. -### link.create +For example: -```ts +```ts title="src/workflows/create-store.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -// ... +const createStoreStep = createStep( + "create-store", + async ({}, { container }) => { + const storeModuleService = container.resolve(Modules.STORE) -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", + const store = await storeModuleService.createStores({ + name: "My Store", + supported_currencies: [{ + currency_code: "usd", + is_default: true, + }], + }) + + return new StepResponse({ store }, store.id) }, -}) + async (storeId, { container }) => { + if(!storeId) { + return + } + const storeModuleService = container.resolve(Modules.STORE) + + await storeModuleService.deleteStores([storeId]) + } +) + +export const createStoreWorkflow = createWorkflow( + "create-store", + () => { + const { store } = createStoreStep() + + return new WorkflowResponse({ store }) + } +) ``` -### createRemoteLinkStep +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +### 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 { createStoreWorkflow } from "../../workflows/create-store" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createStoreWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### 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 { createStoreWorkflow } from "../workflows/create-store" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createStoreWorkflow(container) + .run() -// ... + console.log(result) +} -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) +export const config: SubscriberConfig = { + event: "user.created", +} ``` -*** - -## Product Module - -Medusa defines read-only links between: +### Scheduled Job -- 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. +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createStoreWorkflow } from "../workflows/create-store" -### Retrieve with Query +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createStoreWorkflow(container) + .run() -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`: + console.log(result) +} -To retrieve the product, pass `product.*` in `fields`. +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` -### query.graph +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -```ts -const { data: lineItems } = await query.graph({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) +*** -// lineItems.variant -``` -### useQueryGraphStep +# Links between API Key Module and Other Modules -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +This document showcases the module links defined between the API Key Module and other commerce modules. -// ... +## Summary -const { data: lineItems } = useQueryGraphStep({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) +The API Key Module has the following links to other modules: -// lineItems.variant -``` +- [`ApiKey` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). *** -## Promotion Module +## Sales Channel Module -The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features. +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. -Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart. + - +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. -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. +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 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`. +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: carts } = await query.graph({ - entity: "cart", +const { data: apiKeys } = await query.graph({ + entity: "api_key", fields: [ - "promotions.*", + "sales_channels.*", ], }) -// carts.promotions +// apiKeys.sales_channels ``` ### useQueryGraphStep @@ -17956,19 +17416,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: carts } = useQueryGraphStep({ - entity: "cart", +const { data: apiKeys } = useQueryGraphStep({ + entity: "api_key", fields: [ - "promotions.*", + "sales_channels.*", ], }) -// carts.promotions +// apiKeys.sales_channels ``` ### 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): +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 @@ -17978,11 +17438,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.CART]: { - cart_id: "cart_123", + [Modules.API_KEY]: { + api_key_id: "apk_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", }, }) ``` @@ -17996,76 +17456,79 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", + [Modules.API_KEY]: { + api_key_id: "apk_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", }, }) ``` + +# 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: + +- `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. + +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). + *** -## Region Module +## API Key Expiration -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. +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). -### Retrieve with Query +The associated token is no longer usable or verifiable. -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 +## Token Verification -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "region.*", - ], -}) +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. -// carts.region -``` -### useQueryGraphStep +# Links between Currency Module and Other Modules -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +This document showcases the module links defined between the Currency Module and other commerce modules. -// ... +## Summary -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "region.*", - ], -}) +The Currency Module has the following links to other modules: -// carts.region -``` +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 of Store Module \<> `Currency` data model of Currency Module](#store-module). (Read-only). *** -## Sales Channel Module +## Store 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. +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](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. ### 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`: +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`: ### query.graph ```ts -const { data: carts } = await query.graph({ - entity: "cart", +const { data: stores } = await query.graph({ + entity: "store", fields: [ - "sales_channel.*", + "supported_currencies.currency.*", ], }) -// carts.sales_channel +// stores.supported_currencies ``` ### useQueryGraphStep @@ -18075,89 +17538,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: carts } = useQueryGraphStep({ - entity: "cart", +const { data: stores } = useQueryGraphStep({ + entity: "store", fields: [ - "sales_channel.*", - ], -}) - -// carts.sales_channel -``` - - -# Tax Lines in Cart Module - -In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. - -## 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. - - - -*** - -## 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 them 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 taxes perspective. - - - -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`. - -*** - -## Retrieve Tax Lines - -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. - -```ts -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.tax_lines", - "shipping_methods.tax_lines", - "shipping_address", + "supported_currencies.currency.*", ], }) -// 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", - }, - } -) -``` - -Then, use the returned tax lines to set the line items and shipping methods’ tax lines: - -```ts -// set line item tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "line_item_id" in line) -) - -// set shipping method tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "shipping_line_id" in line) -) +// stores.supported_currencies ``` @@ -18230,208 +17618,6 @@ For example, if you have a custom module with a `Manager` data model, you can au 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). -# Authentication Flows with the Auth Main Service - -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. - -## Authentication Methods - -### Register - -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. - -For example: - -```ts -const data = await authModuleService.register( - "emailpass", - // passed to auth provider - { - // ... - } -) -``` - -This method calls the `register` method of the provider specified in the first parameter and returns its data. - -### Authenticate - -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: - -```ts -const data = await authModuleService.authenticate( - "emailpass", - // passed to auth provider - { - // ... - } -) -``` - -This method calls the `authenticate` method of the provider specified in the first parameter and returns its data. - -*** - -## Auth Flow 1: Basic Authentication - -The basic authentication flow requires first using the `register` method, then the `authenticate` method: - -```ts -const { success, authIdentity, error } = await authModuleService.register( - "emailpass", - // passed to auth provider - { - // ... - } -) - -if (error) { - // registration failed - // TODO return an error - return -} - -// later (can be another route for log-in) -const { success, authIdentity, location } = await authModuleService.authenticate( - "emailpass", - // passed to auth provider - { - // ... - } -) - -if (success && !location) { - // user is authenticated -} -``` - -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. - -Check out the [AuthIdentity](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) reference for the received properties in `authIdentity`. - - - -### Auth Identity with Same Identifier - -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. - -There are two ways to handle this: - -- 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. - -*** - -## Auth Flow 2: Third-Party Service Authentication - -The third-party service authentication method requires using the `authenticate` method first: - -```ts -const { success, authIdentity, location } = await authModuleService.authenticate( - "google", - // passed to auth provider - { - // ... - } -) - -if (location) { - // return the location for the front-end to redirect to -} - -if (!success) { - // authentication failed -} - -// authentication successful -``` - -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. - -For example, when using the `google` provider, the `location` is the URL that the user is navigated to login. - - - -### Overriding Callback URL - -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", - } -) -``` - -### validateCallback - -Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service. - -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 -const { success, authIdentity } = await authModuleService.validateCallback( - "google", - // passed to auth provider - { - // request data, such as - url, - headers, - query, - body, - protocol, - } -) - -if (success) { - // authentication succeeded -} -``` - -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. - - - -*** - -## Reset Password - -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. - -For example: - -```ts -const { success } = await authModuleService.updateProvider( - "emailpass", - // passed to the auth provider - { - entity_id: "user@example.com", - password: "supersecret", - } -) - -if (success) { - // password reset successfully -} -``` - -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. - - # 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. @@ -18743,85 +17929,37 @@ curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/update This API route is useful for providers like `emailpass` that store a user's password and use it for logging them in. -#### 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, `emailpass`. - -#### Pass Token in Authorization Header - -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. - -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. - -### 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. - -For the `emailpass` provider, you must pass the following properties: - -- `email`: The user's email. -- `password`: The new password. - -### Response Fields - -If the authentication is successful, the request returns an object with a `success` property set to `true`: - -```json -{ - "success": "true" -} -``` - - -# Auth Providers - -In this document, you’ll learn how the Auth Module handles authentication using providers. - -## What's an Auth Module Provider? - -An auth module provider handles authenticating customers and users, either using custom logic or by integrating a third-party service. +#### Path Parameters -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. +Its path parameters are: -### Auth Providers List +- `{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`. -- [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) +#### Pass Token in Authorization Header -*** +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. -## Configure Allowed Auth Providers of Actor Types +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. -By default, users of all actor types can authenticate with all installed auth module providers. +### Request Body Parameters -To restrict the auth providers used for actor types, use the [authMethodsPerActor option](https://docs.medusajs.com/references/medusa-config#http-authMethodsPerActor-1-3/index.html.md) in Medusa's configurations: +This route accepts in the request body an object that has the data necessary for the provider to update the user's password. -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - http: { - authMethodsPerActor: { - user: ["google"], - customer: ["emailpass"], - }, - // ... - }, - // ... - }, -}) -``` +For the `emailpass` provider, you must pass the following properties: -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. +- `email`: The user's email. +- `password`: The new password. -*** +### Response Fields -## How to Create an Auth Module Provider +If the authentication is successful, the request returns an object with a `success` property set to `true`: -Refer to [this guide](https://docs.medusajs.com/references/auth/provider/index.html.md) to learn how to create an auth module provider. +```json +{ + "success": "true" +} +``` # How to Create an Actor Type @@ -19217,6 +18355,256 @@ In the workflow, you: You can use this workflow when deleting a manager, such as in an API route. +# Authentication Flows with the Auth Main Service + +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. + +## Authentication Methods + +### Register + +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. + +For example: + +```ts +const data = await authModuleService.register( + "emailpass", + // passed to auth provider + { + // ... + } +) +``` + +This method calls the `register` method of the provider specified in the first parameter and returns its data. + +### Authenticate + +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: + +```ts +const data = await authModuleService.authenticate( + "emailpass", + // passed to auth provider + { + // ... + } +) +``` + +This method calls the `authenticate` method of the provider specified in the first parameter and returns its data. + +*** + +## Auth Flow 1: Basic Authentication + +The basic authentication flow requires first using the `register` method, then the `authenticate` method: + +```ts +const { success, authIdentity, error } = await authModuleService.register( + "emailpass", + // passed to auth provider + { + // ... + } +) + +if (error) { + // registration failed + // TODO return an error + return +} + +// later (can be another route for log-in) +const { success, authIdentity, location } = await authModuleService.authenticate( + "emailpass", + // passed to auth provider + { + // ... + } +) + +if (success && !location) { + // user is authenticated +} +``` + +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. + +Check out the [AuthIdentity](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) reference for the received properties in `authIdentity`. + + + +### Auth Identity with Same Identifier + +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. + +There are two ways to handle this: + +- 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. + +*** + +## Auth Flow 2: Third-Party Service Authentication + +The third-party service authentication method requires using the `authenticate` method first: + +```ts +const { success, authIdentity, location } = await authModuleService.authenticate( + "google", + // passed to auth provider + { + // ... + } +) + +if (location) { + // return the location for the front-end to redirect to +} + +if (!success) { + // authentication failed +} + +// authentication successful +``` + +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. + +For example, when using the `google` provider, the `location` is the URL that the user is navigated to login. + + + +### Overriding Callback URL + +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", + } +) +``` + +### validateCallback + +Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service. + +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 +const { success, authIdentity } = await authModuleService.validateCallback( + "google", + // passed to auth provider + { + // request data, such as + url, + headers, + query, + body, + protocol, + } +) + +if (success) { + // authentication succeeded +} +``` + +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. + + + +*** + +## Reset Password + +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. + +For example: + +```ts +const { success } = await authModuleService.updateProvider( + "emailpass", + // passed to the auth provider + { + entity_id: "user@example.com", + password: "supersecret", + } +) + +if (success) { + // password reset successfully +} +``` + +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? + +An auth module provider handles authenticating customers and users, either using custom logic or by integrating a third-party service. + +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. + +### Auth Providers List + +- [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) + +*** + +## Configure Allowed Auth Providers of Actor Types + +By default, users of all actor types can authenticate with all installed auth module providers. + +To restrict the auth providers used for actor types, use the [authMethodsPerActor option](https://docs.medusajs.com/references/medusa-config#http-authMethodsPerActor-1-3/index.html.md) in Medusa's configurations: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + http: { + authMethodsPerActor: { + user: ["google"], + customer: ["emailpass"], + }, + // ... + }, + // ... + }, +}) +``` + +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. + +*** + +## How to Create an Auth Module Provider + +Refer to [this guide](https://docs.medusajs.com/references/auth/provider/index.html.md) to learn how to create an auth module provider. + + # Auth Module Options In this document, you'll learn about the options of the Auth Module. @@ -19397,184 +18785,201 @@ The page shows the user password fields to enter their new password, then submit - [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) -# Customer Accounts - -In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. - -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. +# Cart Concepts -## `has_account` Property +In this document, you’ll get an overview of the main concepts of a cart. -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. +## Shipping and Billing Addresses -When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. +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). -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`. + *** -## Email Uniqueness - -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. +## Line Items -So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. +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. -# Links between Customer Module and Other Modules +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). -This document showcases the module links defined between the Customer Module and other commerce modules. +*** -## Summary +## Shipping Methods -The Customer Module has the following links to other modules: +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. -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +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. -- [`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). +### data Property -*** +After an order is placed, you can use a third-party fulfillment provider to fulfill its shipments. -## Payment Module +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. -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. +The `data` property is an object used to store custom data relevant later for fulfillment. -This link is available starting from Medusa `v2.5.0`. -### Retrieve with Query +# Promotions Adjustments in Carts -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`: +In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. -### query.graph +## What are Adjustment Lines? -```ts -const { data: customers } = await query.graph({ - entity: "customer", - fields: [ - "account_holder.*", - ], -}) +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. -// customers.account_holder -``` +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. -### useQueryGraphStep + -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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. -// ... +*** -const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: [ - "account_holder.*", - ], -}) +## Discountable Option -// customers.account_holder -``` +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. -### Manage with Link +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. -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 +## Promotion Actions -```ts -import { Modules } from "@medusajs/framework/utils" +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. -// ... +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). -await link.create({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, -}) -``` +For example: -### createRemoteLinkStep +```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + ComputeActionAdjustmentLine, + ComputeActionItemLine, + ComputeActionShippingLine, + // ... +} from "@medusajs/framework/types" -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +// retrieve the cart +const cart = await cartModuleService.retrieveCart("cart_123", { + relations: [ + "items.adjustments", + "shipping_methods.adjustments", + ], +}) -// ... +// retrieve line item adjustments +const lineItemAdjustments: ComputeActionItemLine[] = [] +cart.items.forEach((item) => { + const filteredAdjustments = item.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + lineItemAdjustments.push({ + ...item, + adjustments: filteredAdjustments, + }) + } +}) -createRemoteLinkStep({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, +// retrieve shipping method adjustments +const shippingMethodAdjustments: ComputeActionShippingLine[] = + [] +cart.shipping_methods.forEach((shippingMethod) => { + const filteredAdjustments = + shippingMethod.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + shippingMethodAdjustments.push({ + ...shippingMethod, + adjustments: filteredAdjustments, + }) + } }) + +// compute actions +const actions = await promotionModuleService.computeActions( + ["promo_123"], + { + items: lineItemAdjustments, + shipping_methods: shippingMethodAdjustments, + } +) ``` -*** +The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. -## Cart Module +Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments. -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. +```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + AddItemAdjustmentAction, + AddShippingMethodAdjustment, + // ... +} from "@medusajs/framework/types" -### Retrieve with Query +// ... -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`: +await cartModuleService.setLineItemAdjustments( + cart.id, + actions.filter( + (action) => action.action === "addItemAdjustment" + ) as AddItemAdjustmentAction[] +) -### query.graph +await cartModuleService.setShippingMethodAdjustments( + cart.id, + actions.filter( + (action) => + action.action === "addShippingMethodAdjustment" + ) as AddShippingMethodAdjustment[] +) +``` -```ts -const { data: customers } = await query.graph({ - entity: "customer", - fields: [ - "carts.*", - ], -}) -// customers.carts -``` +# Links between Cart Module and Other Modules -### useQueryGraphStep +This document showcases the module links defined between the Cart Module and other commerce modules. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +## Summary -// ... +The Cart Module has the following links to other modules: -const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: [ - "carts.*", - ], -}) +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -// customers.carts -``` +- [`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). *** -## Order Module +## Customer Module -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. +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. ### Retrieve with Query -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`: +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 ```ts -const { data: customers } = await query.graph({ - entity: "customer", +const { data: carts } = await query.graph({ + entity: "cart", fields: [ - "orders.*", + "customer.*", ], }) -// customers.orders +// carts.order ``` ### useQueryGraphStep @@ -19584,52 +18989,41 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: customers } = useQueryGraphStep({ - entity: "customer", +const { data: carts } = useQueryGraphStep({ + entity: "cart", fields: [ - "orders.*", + "customer.*", ], }) -// customers.orders +// carts.order ``` - -# Links between Currency Module and Other Modules - -This document showcases the module links defined between the Currency Module and other commerce modules. - -## Summary - -The Currency 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 of Store Module \<> `Currency` data model of Currency Module](#store-module). (Read-only). - *** -## Store Module +## Order 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. +The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management features. -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. +Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. + + ### 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 order of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: ### query.graph ```ts -const { data: stores } = await query.graph({ - entity: "store", +const { data: carts } = await query.graph({ + entity: "cart", fields: [ - "supported_currencies.currency.*", + "order.*", ], }) -// stores.supported_currencies +// carts.order ``` ### useQueryGraphStep @@ -19639,161 +19033,211 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stores } = useQueryGraphStep({ - entity: "store", +const { data: carts } = useQueryGraphStep({ + entity: "cart", fields: [ - "supported_currencies.currency.*", + "order.*", ], }) -// stores.supported_currencies +// carts.order ``` +### Manage with Link -# Inventory Concepts +To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. +### link.create -## InventoryItem +```ts +import { Modules } from "@medusajs/framework/utils" -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. +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.ORDER]: { + order_id: "order_123", + }, +}) +``` - +### createRemoteLinkStep -### Inventory Shipping Requirement +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -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). +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.ORDER]: { + order_id: "order_123", + }, +}) +``` *** -## 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. +## Payment Module -### Associated Location +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. -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. +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. -*** + -## ReservationItem +### Retrieve with Query -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. +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`: -The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. +### query.graph +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "payment_collection.*", + ], +}) -# Inventory Module in Medusa Flows +// carts.payment_collection +``` -This document explains how the Inventory Module is used within the Medusa application's flows. +### useQueryGraphStep -## Product Variant Creation +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. +// ... -This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "payment_collection.*", + ], +}) - +// carts.payment_collection +``` -*** +### Manage with Link -## Add to Cart +To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. +### link.create -This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) +```ts +import { Modules } from "@medusajs/framework/utils" - +// ... -*** +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` -## Order Placed +### createRemoteLinkStep -When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. +```ts +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) +// ... - +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` *** -## Order Fulfillment - -When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: - -- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. -- Resets the `reserved_quantity` to `0`. -- Deletes the associated reservation item. - -This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) - - - -*** +## Product Module -## Order Return +Medusa defines read-only links between: -When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. +- 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. -This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) +### 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`: -### Dismissed Returned Items +To retrieve the product, pass `product.*` in `fields`. -If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. +### query.graph +```ts +const { data: lineItems } = await query.graph({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) -# Links between Inventory Module and Other Modules +// lineItems.variant +``` -This document showcases the module links defined between the Inventory Module and other commerce modules. +### useQueryGraphStep -## Summary +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -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. +const { data: lineItems } = useQueryGraphStep({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) -- [`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). +// lineItems.variant +``` *** -## Product Module +## Promotion Module -Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. +The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features. - +Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart. -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). +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. ### 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 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`. ### query.graph ```ts -const { data: inventoryItems } = await query.graph({ - entity: "inventory_item", +const { data: carts } = await query.graph({ + entity: "cart", fields: [ - "variants.*", + "promotions.*", ], }) -// inventoryItems.variants +// carts.promotions ``` ### useQueryGraphStep @@ -19803,19 +19247,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: inventoryItems } = useQueryGraphStep({ - entity: "inventory_item", +const { data: carts } = useQueryGraphStep({ + entity: "cart", fields: [ - "variants.*", + "promotions.*", ], }) -// inventoryItems.variants +// carts.promotions ``` ### Manage with Link -To manage the variants of an inventory item, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -19825,11 +19269,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", + [Modules.PROMOTION]: { + promotion_id: "promo_123", }, }) ``` @@ -19843,36 +19287,36 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", + [Modules.PROMOTION]: { + promotion_id: "promo_123", }, }) ``` *** -## Stock Location Module +## Region Module -Medusa defines a read-only link between the `InventoryLevel` data model and the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md)'s `StockLocation` data model. This means you can retrieve the details of an inventory level's stock locations, 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 `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 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`: +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: inventoryLevels } = await query.graph({ - entity: "inventory_level", +const { data: carts } = await query.graph({ + entity: "cart", fields: [ - "stock_locations.*", + "region.*", ], }) -// inventoryLevels.stock_locations +// carts.region ``` ### useQueryGraphStep @@ -19882,404 +19326,131 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: inventoryLevels } = useQueryGraphStep({ - entity: "inventory_level", +const { data: carts } = useQueryGraphStep({ + entity: "cart", fields: [ - "stock_locations.*", + "region.*", ], }) -// inventoryLevels.stock_locations +// carts.region ``` - -# Inventory Kits - -In this guide, you'll learn how inventory kits can be used in the Medusa application to support use cases like multi-part products, bundled products, and shared inventory across products. - -Refer to the following user guides to learn how to use the Medusa Admin dashboard to: - -- [Create Multi-Part Products](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md). -- [Create Bundled Products](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md). - -## What is an Inventory Kit? - -An inventory kit is a collection of inventory items that are linked to a single product variant. These inventory items can be used to represent different parts of a product, or to represent a bundle of products. - -The Medusa application links inventory items from the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to product variants in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). Each variant can have multiple inventory items, and these inventory items can be re-used or shared across variants. - -Using inventory kits, you can implement use cases like: - -- [Multi-part products](#multi-part-products): A product that consists of multiple parts, each with its own inventory item. -- [Bundled products](#bundled-products): A product that is sold as a bundle, where each variant in the bundle product can re-use the inventory items of another product that should be sold as part of the bundle. - *** -## Multi-Part Products - -Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately. - -To implement this in Medusa, you can: - -- Create inventory items for each of the different parts. -- For each bicycle product, add a variant whose inventory kit consists of the inventory items of each of the parts. - -Then, whenever a customer purchases a bicycle, the inventory of each part is updated accordingly. You can also use the `required_quantity` of the variant's inventory items to set how much quantity is consumed of the part's inventory when a bicycle is sold. For example, the bicycle's wheels require 2 wheels inventory items to be sold when a bicycle is sold. - - - -### Create Multi-Part Product +## Sales Channel Module -Using the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md), you can create a multi-part product by creating its inventory items first, then assigning these inventory items to the product's variant(s). +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. -Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the inventory items: +### Retrieve with Query -```ts highlights={multiPartsHighlights1} -import { - createInventoryItemsWorkflow, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" -import { createWorkflow } from "@medusajs/framework/workflows-sdk" +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`: -export const createMultiPartProductsWorkflow = createWorkflow( - "create-multi-part-products", - () => { - // Alternatively, you can create a stock location - const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: ["*"], - filters: { - name: "European Warehouse", - }, - }) +### query.graph - const inventoryItems = createInventoryItemsWorkflow.runAsStep({ - input: { - items: [ - { - sku: "FRAME", - title: "Frame", - location_levels: [ - { - stocked_quantity: 100, - location_id: stockLocations[0].id, - }, - ], - }, - { - sku: "WHEEL", - title: "Wheel", - location_levels: [ - { - stocked_quantity: 100, - location_id: stockLocations[0].id, - }, - ], - }, - { - sku: "SEAT", - title: "Seat", - location_levels: [ - { - stocked_quantity: 100, - location_id: stockLocations[0].id, - }, - ], - }, - ], - }, - }) +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) - // TODO create the product - } -) +// carts.sales_channel ``` -You start by retrieving the stock location to create the inventory items in. Alternatively, you can [create a stock location](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md). - -Then, you create the inventory items that the product variant consists of. - -Next, create the product and pass the inventory item's IDs to the product's variant: +### useQueryGraphStep -```ts highlights={multiPartHighlights2} -import { - // ... - transform, -} from "@medusajs/framework/workflows-sdk" -import { - // ... - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -export const createMultiPartProductsWorkflow = createWorkflow( - "create-multi-part-products", - () => { - // ... +// ... - const inventoryItemIds = transform({ - inventoryItems, - }, (data) => { - return data.inventoryItems.map((inventoryItem) => { - return { - inventory_item_id: inventoryItem.id, - // can also specify required_quantity - } - }) - }) +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Bicycle", - variants: [ - { - title: "Bicycle - Small", - prices: [ - { - amount: 100, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - inventory_items: inventoryItemIds, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - shipping_profile_id: "sp_123", - }, - ], - }, - }) - } -) +// carts.sales_channel ``` -You prepare the inventory item IDs to pass to the variant using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK, then pass these IDs to the created product's variant. - -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). - -*** - -## Bundled Products -Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle. +# Tax Lines in Cart Module - +In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. -You can do that by creating a product, where each variant re-uses the inventory items of each of the shirt, pants, and shoes products. +## What are Tax Lines? -Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated. +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. - + -### Create Bundled Product +*** -You can create a bundled product in the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md) by creating the products part of the bundle first, each having its own inventory items. Then, you create the bundled product whose variant(s) have inventory kits composed of inventory items from each of the products part of the bundle. +## Tax Inclusivity -Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the products part of the bundle: +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 highlights={bundledHighlights1} -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" +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. -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Shirt", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Shirt", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - { - title: "Pants", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Pants", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - { - title: "Shoes", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Shoes", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - ], - }, - }) +So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. - // TODO re-retrieve with inventory - } -) -``` +The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective. -You create three products and enable `manage_inventory` for their variants, which will create a default inventory item. You can also create the inventory item first for more control over the quantity as explained in [the previous section](#create-multi-part-product). + -Next, retrieve the products again but with variant information: +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`. -```ts highlights={bundledHighlights2} -import { - // ... - transform, -} from "@medusajs/framework/workflows-sdk" -import { - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" +*** -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - // ... - const productIds = transform({ - products, - }, (data) => data.products.map((product) => product.id)) +## Retrieve Tax Lines - // @ts-ignore - const { data: productsWithInventory } = useQueryGraphStep({ - entity: "product", - fields: [ - "variants.*", - "variants.inventory_items.*", - ], - filters: { - id: productIds, - }, - }) +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. - const inventoryItemIds = transform({ - productsWithInventory, - }, (data) => { - return data.productsWithInventory.map((product) => { - return { - inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id, - } - }) - }) +```ts +// retrieve the cart +const cart = await cartModuleService.retrieveCart("cart_123", { + relations: [ + "items.tax_lines", + "shipping_methods.tax_lines", + "shipping_address", + ], +}) - // create bundled product +// 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", + }, } ) ``` -Using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), you retrieve the product again with the inventory items of each variant. Then, you prepare the inventory items to pass to the bundled product's variant. +Then, use the returned tax lines to set the line items and shipping methods’ tax lines: -Finally, create the bundled product: +```ts +// set line item tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "line_item_id" in line) +) -```ts highlights={bundledProductHighlights3} -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - // ... - const bundledProduct = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Bundled Clothes", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Bundle", - prices: [ - { - amount: 30, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - inventory_items: inventoryItemIds, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - ], - }, - }).config({ name: "create-bundled-product" }) - } +// set shipping method tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "shipping_line_id" in line) ) ``` -The bundled product has the same inventory items as those of the products part of the bundle. - -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). - # Fulfillment Concepts @@ -20327,33 +19498,6 @@ A shipping profile defines a type of items that are shipped in a similar manner. 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. -# 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. @@ -20407,6 +19551,33 @@ The `Fulfillment` data model has three properties to keep track of the current s - `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered. +# 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. + + # Links between Fulfillment Module and Other Modules This document showcases the module links defined between the Fulfillment Module and other commerce modules. @@ -20862,211 +20033,550 @@ module.exports = defineConfig({ }, ], }, - }, - ], -}) + }, + ], +}) +``` + +The `providers` option is an array of objects that accept the following properties: + +- `resolve`: A string indicating either 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. + + +# Inventory Module in Medusa Flows + +This document explains how the Inventory Module is used within the Medusa application's flows. + +## Product Variant Creation + +When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. + +This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) + + + +*** + +## Add to Cart + +When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. + +This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) + + + +*** + +## Order Placed + +When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. + +This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) + + + +*** + +## Order Fulfillment + +When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: + +- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. +- Resets the `reserved_quantity` to `0`. +- Deletes the associated reservation item. + +This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) + + + +*** + +## Order Return + +When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. + +This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) + + + +### Dismissed Returned Items + +If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. + + +# Inventory Kits + +In this guide, you'll learn how inventory kits can be used in the Medusa application to support use cases like multi-part products, bundled products, and shared inventory across products. + +Refer to the following user guides to learn how to use the Medusa Admin dashboard to: + +- [Create Multi-Part Products](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md). +- [Create Bundled Products](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md). + +## What is an Inventory Kit? + +An inventory kit is a collection of inventory items that are linked to a single product variant. These inventory items can be used to represent different parts of a product, or to represent a bundle of products. + +The Medusa application links inventory items from the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to product variants in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). Each variant can have multiple inventory items, and these inventory items can be re-used or shared across variants. + +Using inventory kits, you can implement use cases like: + +- [Multi-part products](#multi-part-products): A product that consists of multiple parts, each with its own inventory item. +- [Bundled products](#bundled-products): A product that is sold as a bundle, where each variant in the bundle product can re-use the inventory items of another product that should be sold as part of the bundle. + +*** + +## Multi-Part Products + +Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately. + +To implement this in Medusa, you can: + +- Create inventory items for each of the different parts. +- For each bicycle product, add a variant whose inventory kit consists of the inventory items of each of the parts. + +Then, whenever a customer purchases a bicycle, the inventory of each part is updated accordingly. You can also use the `required_quantity` of the variant's inventory items to set how much quantity is consumed of the part's inventory when a bicycle is sold. For example, the bicycle's wheels require 2 wheels inventory items to be sold when a bicycle is sold. + + + +### Create Multi-Part Product + +Using the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md), you can create a multi-part product by creating its inventory items first, then assigning these inventory items to the product's variant(s). + +Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the inventory items: + +```ts highlights={multiPartsHighlights1} +import { + createInventoryItemsWorkflow, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" +import { createWorkflow } from "@medusajs/framework/workflows-sdk" + +export const createMultiPartProductsWorkflow = createWorkflow( + "create-multi-part-products", + () => { + // Alternatively, you can create a stock location + const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: ["*"], + filters: { + name: "European Warehouse", + }, + }) + + const inventoryItems = createInventoryItemsWorkflow.runAsStep({ + input: { + items: [ + { + sku: "FRAME", + title: "Frame", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id, + }, + ], + }, + { + sku: "WHEEL", + title: "Wheel", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id, + }, + ], + }, + { + sku: "SEAT", + title: "Seat", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id, + }, + ], + }, + ], + }, + }) + + // TODO create the product + } +) +``` + +You start by retrieving the stock location to create the inventory items in. Alternatively, you can [create a stock location](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md). + +Then, you create the inventory items that the product variant consists of. + +Next, create the product and pass the inventory item's IDs to the product's variant: + +```ts highlights={multiPartHighlights2} +import { + // ... + transform, +} from "@medusajs/framework/workflows-sdk" +import { + // ... + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +export const createMultiPartProductsWorkflow = createWorkflow( + "create-multi-part-products", + () => { + // ... + + const inventoryItemIds = transform({ + inventoryItems, + }, (data) => { + return data.inventoryItems.map((inventoryItem) => { + return { + inventory_item_id: inventoryItem.id, + // can also specify required_quantity + } + }) + }) + + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Bicycle", + variants: [ + { + title: "Bicycle - Small", + prices: [ + { + amount: 100, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + inventory_items: inventoryItemIds, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + shipping_profile_id: "sp_123", + }, + ], + }, + }) + } +) ``` -The `providers` option is an array of objects that accept the following properties: - -- `resolve`: A string indicating either 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. - - -# Order Concepts - -In this document, you’ll learn about orders and related concepts - -## Order Items - -The items purchased in the order are represented by the [OrderItem data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). An order can have multiple items. - - - -### Item’s Product Details +You prepare the inventory item IDs to pass to the variant using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK, then pass these IDs to the created product's variant. -The details of the purchased products are represented by the [LineItem data model](https://docs.medusajs.com/references/order/models/OrderLineItem/index.html.md). Not only does a line item hold the details of the product, but also details related to its price, adjustments due to promotions, and taxes. +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). *** -## Order’s Shipping Method +## Bundled Products -An order has one or more shipping methods used to handle item shipment. +Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle. -Each shipping method is represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md) that holds its details. The shipping method is linked to the order through the [OrderShipping data model](https://docs.medusajs.com/references/order/models/OrderShipping/index.html.md). + - +You can do that by creating a product, where each variant re-uses the inventory items of each of the shirt, pants, and shoes products. -### data Property +Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated. -When fulfilling the order, you can use a third-party fulfillment provider that requires additional custom data to be passed along from the order creation process. + -The `OrderShippingMethod` data model has a `data` property. It’s an object used to store custom data relevant later for fulfillment. +### Create Bundled Product -The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items. +You can create a bundled product in the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md) by creating the products part of the bundle first, each having its own inventory items. Then, you create the bundled product whose variant(s) have inventory kits composed of inventory items from each of the products part of the bundle. -*** +Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the products part of the bundle: -## Order Totals +```ts highlights={bundledHighlights1} +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" -The order’s total amounts (including tax total, total after an item is returned, etc…) are represented by the [OrderSummary data model](https://docs.medusajs.com/references/order/models/OrderSummary/index.html.md). +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Shirt", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Shirt", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + { + title: "Pants", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Pants", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + { + title: "Shoes", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Shoes", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + ], + }, + }) -*** + // TODO re-retrieve with inventory + } +) +``` -## Order Payments +You create three products and enable `manage_inventory` for their variants, which will create a default inventory item. You can also create the inventory item first for more control over the quantity as explained in [the previous section](#create-multi-part-product). -Payments made on an order, whether they’re capture or refund payments, are recorded as transactions represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). +Next, retrieve the products again but with variant information: -An order can have multiple transactions. The sum of these transactions must be equal to the order summary’s total. Otherwise, there’s an outstanding amount. +```ts highlights={bundledHighlights2} +import { + // ... + transform, +} from "@medusajs/framework/workflows-sdk" +import { + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" -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). +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + // ... + const productIds = transform({ + products, + }, (data) => data.products.map((product) => product.id)) + // @ts-ignore + const { data: productsWithInventory } = useQueryGraphStep({ + entity: "product", + fields: [ + "variants.*", + "variants.inventory_items.*", + ], + filters: { + id: productIds, + }, + }) -# Order Edit + const inventoryItemIds = transform({ + productsWithInventory, + }, (data) => { + return data.productsWithInventory.map((product) => { + return { + inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id, + } + }) + }) -In this document, you'll learn about order edits. + // create bundled product + } +) +``` -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. +Using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), you retrieve the product again with the inventory items of each variant. Then, you prepare the inventory items to pass to the bundled product's variant. -## What is an Order Edit? +Finally, create the bundled product: -A merchant can edit an order to add new items or change the quantity of existing items in the order. +```ts highlights={bundledProductHighlights3} +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + // ... + const bundledProduct = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Bundled Clothes", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Bundle", + prices: [ + { + amount: 30, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + inventory_items: inventoryItemIds, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + ], + }, + }).config({ name: "create-bundled-product" }) + } +) +``` -An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md). +The bundled product has the same inventory items as those of the products part of the bundle. -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. +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). -In the case of an order edit, the `OrderChange`'s type is `edit`. -*** +# Inventory Concepts -## Add Items in an Order Edit +In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. -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). +## InventoryItem -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. +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. -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. +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. -*** + -## Update Items in an Order Edit +### Inventory Shipping Requirement -A merchant can update an existing item's quantity or price. +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. -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. +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). *** -## Shipping Methods of New Items in the Edit +## InventoryLevel -Adding new items to the order requires adding shipping methods for those items. +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. -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` +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. -## How Order Edits Impact an Order’s Version +### Associated Location -When an order edit is confirmed, the order’s version is incremented. +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. *** -## Payments and Refunds for Order Edit Changes +## ReservationItem -Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order. +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. -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). +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 Order Module and Other Modules +# Links between Inventory Module and Other Modules -This document showcases the module links defined between the Order Module and other commerce modules. +This document showcases the module links defined between the Inventory Module and other commerce modules. ## Summary -The Order Module has the following links to other modules: +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. -- [`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). +- [`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). *** -## Customer Module - -Medusa defines a read-only link between the `Order` 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 an order's customer, 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. - -### Retrieve with Query - -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`: - -### query.graph - -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "customer.*", - ], -}) - -// orders.customer -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "customer.*", - ], -}) - -// orders.customer -``` - -*** +## Product Module -## Cart Module +Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. -The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) provides cart-management features. + -Medusa defines a link between the `Order` and `Cart` data models. The order is linked to the cart used for the purchased. +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 cart of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` 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 ```ts -const { data: orders } = await query.graph({ - entity: "order", +const { data: inventoryItems } = await query.graph({ + entity: "inventory_item", fields: [ - "cart.*", + "variants.*", ], }) -// orders.cart +// inventoryItems.variants ``` ### useQueryGraphStep @@ -21076,19 +20586,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: inventoryItems } = useQueryGraphStep({ + entity: "inventory_item", fields: [ - "cart.*", + "variants.*", ], }) -// orders.cart +// inventoryItems.variants ``` ### Manage with Link -To manage the cart of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the variants of an inventory item, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -21098,11 +20608,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.CART]: { - cart_id: "cart_123", + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", }, }) ``` @@ -21116,44 +20626,36 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.CART]: { - cart_id: "cart_123", + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", }, }) ``` *** -## Fulfillment Module - -A fulfillment is created for an orders' items. Medusa defines a link between the `Fulfillment` and `Order` data models. - - - -A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. +## Stock Location Module - +Medusa defines a read-only link between the `InventoryLevel` data model and the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md)'s `StockLocation` data model. This means you can retrieve the details of an inventory level's stock locations, 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 fulfillments of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillments.*` in `fields`: - -To retrieve the fulfillments of a return, pass `fulfillments.*` in `fields`. +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: orders } = await query.graph({ - entity: "order", +const { data: inventoryLevels } = await query.graph({ + entity: "inventory_level", fields: [ - "fulfillments.*", + "stock_locations.*", ], }) -// orders.fulfillments +// inventoryLevels.stock_locations ``` ### useQueryGraphStep @@ -21163,343 +20665,228 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: inventoryLevels } = useQueryGraphStep({ + entity: "inventory_level", fields: [ - "fulfillments.*", + "stock_locations.*", ], }) -// orders.fulfillments +// inventoryLevels.stock_locations ``` -### Manage with Link -To manage the fulfillments of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +# Order Claim -### link.create +In this document, you’ll learn about order claims. -```ts -import { Modules } from "@medusajs/framework/utils" +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 a Claim? -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", - }, -}) -``` +When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. -### createRemoteLinkStep +The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +*** -// ... +## Claim Type -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", - }, -}) -``` +The `Claim` data model has a `type` property whose value indicates the type of the claim: + +- `refund`: the items are returned, and the customer is refunded. +- `replace`: the items are returned, and the customer receives new items. *** -## Payment Module +## Old and Replacement Items -An order's payment details are stored in a payment collection. This also applies for claims and exchanges. +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. -So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models. +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). -### Retrieve with Query +*** -To retrieve the payment collections of an order, order exchange, or order claim with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collections.*` in `fields`: +## Claim Shipping Methods -### query.graph +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). -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "payment_collections.*", - ], -}) +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). -// orders.payment_collections -``` +*** -### useQueryGraphStep +## Claim Refund -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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. -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "payment_collections.*", - ], -}) +*** -// orders.payment_collections -``` +## How Claims Impact an Order’s Version -### Manage with Link +When a claim is confirmed, the order’s version is incremented. -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 +# Order Edit -```ts -import { Modules } from "@medusajs/framework/utils" +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. -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` +## What is an Order Edit? -### createRemoteLinkStep +A merchant can edit an order to add new items or change the quantity of existing items in the order. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md). -// ... +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. -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` +In the case of an order edit, the `OrderChange`'s type is `edit`. *** -## Product Module +## Add Items in an Order Edit -Medusa defines read-only links between: +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). -- the `OrderLineItem` 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 `OrderLineItem` data model. -- the `OrderLineItem` 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 `OrderLineItem` data model. +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. -### Retrieve with Query +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. -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 product, pass `product.*` in `fields`. +## Update Items in an Order Edit -### query.graph +A merchant can update an existing item's quantity or price. -```ts -const { data: lineItems } = await query.graph({ - entity: "order_line_item", - fields: [ - "variant.*", - ], -}) +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. -// lineItems.variant -``` +*** -### useQueryGraphStep +## Shipping Methods of New Items in the Edit -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +Adding new items to the order requires adding shipping methods for those items. -// ... +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` -const { data: lineItems } = useQueryGraphStep({ - entity: "order_line_item", - fields: [ - "variant.*", - ], -}) +*** -// lineItems.variant -``` +## How Order Edits Impact an Order’s Version + +When an order edit is confirmed, the order’s version is incremented. *** -## Promotion Module +## Payments and Refunds for Order Edit Changes -An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. +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). -### Retrieve with Query -To retrieve the promotion applied on an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotion.*` in `fields`: +# Order Concepts -### query.graph +In this document, you’ll learn about orders and related concepts -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "promotion.*", - ], -}) +## Order Items -// orders.promotion -``` +The items purchased in the order are represented by the [OrderItem data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). An order can have multiple items. -### useQueryGraphStep + -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +### Item’s Product Details -// ... +The details of the purchased products are represented by the [LineItem data model](https://docs.medusajs.com/references/order/models/OrderLineItem/index.html.md). Not only does a line item hold the details of the product, but also details related to its price, adjustments due to promotions, and taxes. -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "promotion.*", - ], -}) +*** -// orders.promotion -``` +## Order’s Shipping Method -### Manage with Link +An order has one or more shipping methods used to handle item shipment. -To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +Each shipping method is represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md) that holds its details. The shipping method is linked to the order through the [OrderShipping data model](https://docs.medusajs.com/references/order/models/OrderShipping/index.html.md). -### link.create + -```ts -import { Modules } from "@medusajs/framework/utils" +### data Property -// ... +When fulfilling the order, you can use a third-party fulfillment provider that requires additional custom data to be passed along from the order creation process. -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` +The `OrderShippingMethod` data model has a `data` property. It’s an object used to store custom data relevant later for fulfillment. -### createRemoteLinkStep +The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +*** -// ... +## Order Totals -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` +The order’s total amounts (including tax total, total after an item is returned, etc…) are represented by the [OrderSummary data model](https://docs.medusajs.com/references/order/models/OrderSummary/index.html.md). *** -## Region Module +## Order Payments -Medusa defines a read-only link between the `Order` 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 an order's region, 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. +Payments made on an order, whether they’re capture or refund payments, are recorded as transactions represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). -### Retrieve with Query +An order can have multiple transactions. The sum of these transactions must be equal to the order summary’s total. Otherwise, there’s an outstanding amount. -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`: +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). -### query.graph -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "region.*", - ], -}) +# Order Exchange -// orders.region -``` +In this document, you’ll learn about order exchanges. -### useQueryGraphStep +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +## What is an Exchange? -// ... +An exchange is the replacement of an item that the customer ordered with another. -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "region.*", - ], -}) +A merchant creates the exchange, specifying the items to be replaced and the new items to be sent. -// orders.region -``` +The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. *** -## Sales Channel Module +## Returned and New Items -Medusa defines a read-only link between the `Order` 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 an order's sales channel, 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. +When the exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is created to handle receiving the items back from the customer. -### Retrieve with Query +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). -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`: +The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer. -### query.graph +*** -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "sales_channel.*", - ], -}) +## Exchange Shipping Methods -// orders.sales_channel -``` +An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). -### useQueryGraphStep +The shipping methods for the returned items are associated with the exchange'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). -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +*** -// ... +## Exchange Payment -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "sales_channel.*", - ], -}) +The `Exchange` data model has a `difference_due` property that stores the outstanding amount. -// orders.sales_channel -``` +|Condition|Result| +|---|---|---| +|\`difference\_due \< 0\`|Merchant owes the customer a refund of the | +|\`difference\_due > 0\`|Merchant requires additional payment from the customer of the | +|\`difference\_due = 0\`|No payment processing is required.| + +Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). + +*** + +## How Exchanges Impact an Order’s Version + +When an exchange is confirmed, the order’s version is incremented. # Order Change @@ -21541,36 +20928,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\`| -# Order Versioning - -In this document, you’ll learn how an order and its details are versioned. - -## What's Versioning? - -Versioning means assigning a version number to a record, such as an order and its items. This is useful to view the different versions of the order following changes in its lifetime. - -When changes are made on an order, such as an item is added or returned, the order's version changes. - -*** - -## version Property - -The `Order` and `OrderSummary` data models have a `version` property that indicates the current version. By default, its value is `1`. - -Other order-related data models, such as `OrderItem`, also has a `version` property, but it indicates the version it belongs to. - -*** - -## How the Version Changes - -When the order is changed, such as an item is exchanged, this changes the version of the order and its related data: - -1. The version of the order and its summary is incremented. -2. Related order data that have a `version` property, such as the `OrderItem`, are duplicated. The duplicated item has the new version, whereas the original item has the previous version. - -When the order is retrieved, only the related data having the same version is retrieved. - - # Promotions Adjustments in Orders In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines. @@ -21693,6 +21050,67 @@ await orderModuleService.setOrderShippingMethodAdjustments( ``` +# Order Return + +In this document, you’ll learn about order returns. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard. + +## What is a Return? + +A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md). + +A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow. + + + +Once the merchant receives the returned items, they mark the return as received. + +*** + +## Returned Items + +The items to be returned are represented by the [ReturnItem data model](references/order/models/ReturnItem). + +The `ReturnItem` model has two properties storing the item's quantity: + +1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity. +2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity. + +*** + +## Return Shipping Methods + +A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](references/order/models/OrderShippingMethod). + +In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled. + +*** + +## Refund Payment + +The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer. + +The [OrderTransaction data model](references/order/models/OrderTransaction) represents the refunds made for the return. + +*** + +## Returns in Exchanges and Claims + +When a merchant creates an exchange or a claim, it includes returning items from the customer. + +The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for. + +*** + +## How Returns Impact an Order’s Version + +The order’s version is incremented when: + +1. A return is requested. +2. A return is marked as received. + + # Tax Lines in Order Module In this document, you’ll learn about tax lines in an order. @@ -21770,87 +21188,123 @@ 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`. -# Account Holders and Saved Payment Methods +# Order Versioning -In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. +In this document, you’ll learn how an order and its details are versioned. -Account holders are available starting from Medusa `v2.5.0`. +## What's Versioning? -## What's an Account Holder? +Versioning means assigning a version number to a record, such as an order and its items. This is useful to view the different versions of the order following changes in its lifetime. -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. +When changes are made on an order, such as an item is added or returned, the order's version changes. -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. +## version Property -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. +The `Order` and `OrderSummary` data models have a `version` property that indicates the current version. By default, its value is `1`. + +Other order-related data models, such as `OrderItem`, also has a `version` property, but it indicates the version it belongs to. *** -## Save Payment Methods +## How the Version Changes -If a payment provider supports saving payment methods for a customer, they must implement the following methods: +When the order is changed, such as an item is exchanged, this changes the version of the order and its related data: -- `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. +1. The version of the order and its summary is incremented. +2. Related order data that have a `version` property, such as the `OrderItem`, are duplicated. The duplicated item has the new version, whereas the original item has the previous version. + +When the order is retrieved, only the related data having the same version is retrieved. -Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). + +# Links between Order Module and Other Modules + +This document showcases the module links defined between the Order Module and other commerce modules. + +## Summary + +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). *** -## Account Holder in Medusa Payment Flows +## Customer 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. +Medusa defines a read-only link between the `Order` 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 an order's customer, 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. -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. +### Retrieve with Query -This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). +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`: +### query.graph -# Links between Payment Module and Other Modules +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "customer.*", + ], +}) -This document showcases the module links defined between the Payment Module and other commerce modules. +// orders.customer +``` -## Summary +### useQueryGraphStep -The Payment Module has the following links to other modules: +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -- [`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). +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "customer.*", + ], +}) + +// orders.customer +``` *** ## Cart Module -The Cart Module provides cart-related features, but not payment processing. +The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) provides cart-management features. -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. +Medusa defines a link between the `Order` and `Cart` data models. The order is linked to the cart used for the purchased. -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 -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`: +To retrieve the cart of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`: ### query.graph ```ts -const { data: paymentCollections } = await query.graph({ - entity: "payment_collection", +const { data: orders } = await query.graph({ + entity: "order", fields: [ "cart.*", ], }) -// paymentCollections.cart +// orders.cart ``` ### useQueryGraphStep @@ -21860,19 +21314,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: paymentCollections } = useQueryGraphStep({ - entity: "payment_collection", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ "cart.*", ], }) -// paymentCollections.cart +// orders.cart ``` ### Manage with Link -To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the cart of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -21882,55 +21336,62 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, [Modules.CART]: { cart_id: "cart_123", }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, }) ``` ### createRemoteLinkStep ```ts +import { Modules } from "@medusajs/framework/utils" import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, [Modules.CART]: { cart_id: "cart_123", }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, }) ``` *** -## Customer Module +## Fulfillment 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. +A fulfillment is created for an orders' items. Medusa defines a link between the `Fulfillment` and `Order` data models. -This link is available starting from Medusa `v2.5.0`. + + +A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. + + ### Retrieve with Query -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`: +To retrieve the fulfillments of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillments.*` in `fields`: + +To retrieve the fulfillments of a return, pass `fulfillments.*` in `fields`. ### query.graph ```ts -const { data: accountHolders } = await query.graph({ - entity: "account_holder", +const { data: orders } = await query.graph({ + entity: "order", fields: [ - "customer.*", + "fulfillments.*", ], }) -// accountHolders.customer +// orders.fulfillments ``` ### useQueryGraphStep @@ -21940,19 +21401,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: accountHolders } = useQueryGraphStep({ - entity: "account_holder", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ - "customer.*", + "fulfillments.*", ], }) -// accountHolders.customer +// orders.fulfillments ``` ### Manage with Link -To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the fulfillments of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -21962,11 +21423,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", }, }) ``` @@ -21974,23 +21435,24 @@ await link.create({ ### createRemoteLinkStep ```ts +import { Modules } from "@medusajs/framework/utils" import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", }, }) ``` *** -## Order Module +## Payment Module An order's payment details are stored in a payment collection. This also applies for claims and exchanges. @@ -22000,19 +21462,19 @@ So, Medusa defines links between the `PaymentCollection` data model and the `Ord ### Retrieve with Query -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`: +To retrieve the payment collections of an order, order exchange, or order claim with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collections.*` in `fields`: ### query.graph ```ts -const { data: paymentCollections } = await query.graph({ - entity: "payment_collection", +const { data: orders } = await query.graph({ + entity: "order", fields: [ - "order.*", + "payment_collections.*", ], }) -// paymentCollections.order +// orders.payment_collections ``` ### useQueryGraphStep @@ -22022,14 +21484,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: paymentCollections } = useQueryGraphStep({ - entity: "payment_collection", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ - "order.*", + "payment_collections.*", ], }) -// paymentCollections.order +// orders.payment_collections ``` ### Manage with Link @@ -22073,29 +21535,72 @@ createRemoteLinkStep({ *** -## Region Module +## Product Module -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. +Medusa defines read-only links between: - +- the `OrderLineItem` 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 `OrderLineItem` data model. +- the `OrderLineItem` 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 `OrderLineItem` data model. -This increases the flexibility of your store. For example, you only show during checkout the payment providers associated with the cart's region. +### 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`: + +To retrieve the product, pass `product.*` in `fields`. + +### query.graph + +```ts +const { data: lineItems } = await query.graph({ + entity: "order_line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems.variant +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: lineItems } = useQueryGraphStep({ + entity: "order_line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems.variant +``` + +*** + +## Promotion Module + +An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. + + ### 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`: +To retrieve the promotion applied on an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotion.*` in `fields`: ### query.graph ```ts -const { data: paymentProviders } = await query.graph({ - entity: "payment_provider", +const { data: orders } = await query.graph({ + entity: "order", fields: [ - "regions.*", + "promotion.*", ], }) -// paymentProviders.regions +// orders.promotion ``` ### useQueryGraphStep @@ -22105,19 +21610,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: paymentProviders } = useQueryGraphStep({ - entity: "payment_provider", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ - "regions.*", + "promotion.*", ], }) -// paymentProviders.regions +// orders.promotion ``` ### 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 promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -22127,11 +21632,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.REGION]: { - region_id: "reg_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", + [Modules.PROMOTION]: { + promotion_id: "promo_123", }, }) ``` @@ -22145,634 +21650,873 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.REGION]: { - region_id: "reg_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", + [Modules.PROMOTION]: { + promotion_id: "promo_123", }, }) ``` +*** -# Payment Module Options - -In this document, you'll learn about the options of the Payment Module. +## Region Module -## All Module Options +Medusa defines a read-only link between the `Order` 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 an order's region, 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. -|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|-| +### 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`: -## providers Option +### query.graph -The `providers` option is an array of payment module providers. +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "region.*", + ], +}) -When the Medusa application starts, these providers are registered and can be used to process payments. +// orders.region +``` -For example: +### useQueryGraphStep -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/payment", - options: { - providers: [ - { - resolve: "@medusajs/medusa/payment-stripe", - id: "stripe", - options: { - // ... - }, - }, - ], - }, - }, +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "region.*", ], }) + +// orders.region ``` -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. +## Sales Channel Module +Medusa defines a read-only link between the `Order` 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 an order's sales channel, 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. -# Payment +### Retrieve with Query -In this document, you’ll learn what a payment is and how it's created, captured, and refunded. +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`: -## What's a Payment? +### query.graph -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. +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "sales_channel.*", + ], +}) -A payment carries many of the data and relations of a payment session: +// orders.sales_channel +``` -- 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. +### useQueryGraphStep -*** +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -## Capture Payments +// ... -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. +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "sales_channel.*", + ], +}) -The payment can also be captured incrementally, each time a capture record is created for that amount. +// orders.sales_channel +``` - -*** +# Pricing Concepts -## Refund Payments +In this document, you’ll learn about the main concepts in the Pricing Module. -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 Set -A payment can be refunded multiple times, and each time a refund record is created. +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). - +Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). + -# Payment Collection +*** -In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module. +## Price List -## What's a Payment Collection? +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 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). +A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied. -Every purchase or request for payment starts with a payment collection. The collection holds details necessary to complete the payment, including: +Its associated prices are represented by the `Price` data model. -- 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 Pricing Module and Other Modules -## Multiple Payments +This document showcases the module links defined between the Pricing Module and other commerce modules. -The payment collection supports multiple payment sessions and payments. +## Summary -You can use this to accept payments in increments or split payments across payment providers. +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). *** -## Usage with the Cart Module +## Fulfillment Module -The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment. +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. -During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing. +Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. -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 +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`: -# Accept Payment Flow +### query.graph -In this document, you’ll learn how to implement an accept-payment flow using workflows or the Payment Module's main service. +```ts +const { data: priceSets } = await query.graph({ + entity: "price_set", + fields: [ + "shipping_option.*", + ], +}) -It's highly recommended to use Medusa's workflows to implement this flow. Use the Payment Module's main service for more complex cases. +// priceSets.shipping_option +``` -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). +### useQueryGraphStep -## Flow Overview +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - +// ... -*** +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", + fields: [ + "shipping_option.*", + ], +}) -## 1. Create a Payment Collection +// priceSets.shipping_option +``` -A payment collection holds all details related to a resource’s payment operations. So, you start off by creating a payment collection. +### Manage with Link -For example: +To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -### Using Workflow +### link.create ```ts -import { createPaymentCollectionForCartWorkflow } from "@medusajs/medusa/core-flows" +import { Modules } from "@medusajs/framework/utils" // ... -await createPaymentCollectionForCartWorkflow(req.scope) - .run({ - input: { - cart_id: "cart_123", - }, - }) +await link.create({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) ``` -### Using Service +### createRemoteLinkStep ```ts -const paymentCollection = - await paymentModuleService.createPaymentCollections({ - currency_code: "usd", - amount: 5000, - }) +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) ``` *** -## 2. Create Payment Sessions +## Product Module -The payment collection has one or more payment sessions, each being a payment amount to be authorized by a payment provider. +The Product Module doesn't store or manage the prices of product variants. -So, after creating the payment collection, create at least one payment session for a provider. +Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variant’s prices are stored as prices belonging to a price set. -For example: + -### Using Workflow +So, when you want to add prices for a product variant, you create a price set and add the prices to it. -```ts -import { createPaymentSessionsWorkflow } from "@medusajs/medusa/core-flows" +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 -const { result: paymentSesion } = await createPaymentSessionsWorkflow(req.scope) - .run({ - input: { - payment_collection_id: "paycol_123", - provider_id: "stripe", - }, - }) -``` +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`: -### Using Service +### query.graph ```ts -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: priceSets } = await query.graph({ + entity: "price_set", + fields: [ + "variant.*", + ], +}) + +// priceSets.variant ``` -*** +### useQueryGraphStep -## 3. Authorize Payment Session +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -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. +// ... -For example: +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", + fields: [ + "variant.*", + ], +}) -### Using Step +// priceSets.variant +``` + +### 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): + +### link.create ```ts -import { authorizePaymentSessionStep } from "@medusajs/medusa/core-flows" +import { Modules } from "@medusajs/framework/utils" // ... -authorizePaymentSessionStep({ - id: "payses_123", - context: {}, +await link.create({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, }) ``` -### Using Service +### createRemoteLinkStep ```ts -const payment = authorizePaymentSessionStep({ - id: "payses_123", - context: {}, +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, }) ``` -When the payment authorization is successful, a payment is created and returned. -### Handling Additional Action +# Price Rules -If you used the `authorizePaymentSessionStep`, you don't need to implement this logic as it's implemented in the step. +In this document, you'll learn about price rules for price sets and price lists. -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. +## Price Rule -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. +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. + +For exmaple, you create a price restricted to `10557` zip codes. + + + +A price can have multiple price rules. + +For example, a price can be restricted by a region and a zip code. + + + +*** + +## Price List Rules + +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. + + + + +# Prices Calculation + +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 + +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. + +It returns a price object with the best matching price for each price set. + +### Calculation 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. For example: ```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] - - if (updatedPaymentSession.status === "requires_more") { - // TODO perform required action - // TODO authorize payment again. +const price = await pricingModuleService.calculatePrices( + { id: [priceSetId] }, + { + context: { + currency_code: currencyCode, + region_id: "reg_123", + }, } -} +) ``` -*** +In this example, you retrieve the prices in a price set for the specified currency code and region ID. -## 4. Payment Flow Complete +### Returned Price Object -The payment flow is complete once the payment session is authorized and the payment is created. +For each price set, the `calculatePrices` method selects two prices: -You can then: +- 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. -- 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). +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. -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 Module Provider + - max\_quantity: (\`number\`) The price's max quantity condition. +- original\_price: (\`object\`) The original price's price details. -In this document, you’ll learn what a payment module provider is. + - id: (\`string\`) The ID of the price. -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. + - price\_list\_id: (\`string\`) The ID of the associated price list. -## What's a Payment Module Provider? + - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. -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. + - min\_quantity: (\`number\`) The price's min quantity condition. -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. + - max\_quantity: (\`number\`) The price's max quantity condition. -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. +*** -### List of Payment Module Providers +## Examples -- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md) +Consider the following price set: -*** +```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", + }, + }, + ], +}) +``` -## System Payment Provider +### Default Price Selection -The Payment Module provides a `system` payment provider that acts as a placeholder payment provider. +### Code -It doesn’t handle payment processing and delegates that to the merchant. It acts similarly to a cash-on-delivery (COD) payment method. +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR" + } + } +) +``` -*** +### Result -## How are Payment Providers Created? +### Calculate Prices with Rules -A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`. +### Code -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. +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR", + region_id: "reg_123", + city: "krakow" + } + } +) +``` -*** +### Result -## Configure Payment Providers +### Price Selection with Price List -The Payment Module accepts a `providers` option that allows you to register providers in your application. +### Code -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). +```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" + } + } +) +``` -## PaymentProvider Data Model +### Result -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. +# Account Holders and Saved Payment Methods +In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. -# Payment Session +Account holders are available starting from Medusa `v2.5.0`. -In this document, you’ll learn what a payment session is. +## What's an Account Holder? -## What's a Payment Session? +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 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. +It holds fields retrieved from the third-party provider, such as: -A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. +- `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. - +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. *** -## data Property +## Save Payment Methods -Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data. +If a payment provider supports saving payment methods for a customer, they must implement the following methods: -For example, the customer's ID in Stripe is stored in the `data` property. +- `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. + +Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). *** -## Payment Session Status +## Account Holder in Medusa Payment Flows -The `status` property of a payment session indicates its current status. Its value can be: +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. -- `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. +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. + +This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). -# Webhook Events +# Tax-Inclusive Pricing -In this document, you’ll learn how the Payment Module supports listening to webhook events. +In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. -## What's a Webhook Event? +## What is Tax-Inclusive Pricing? -A webhook event is sent from a third-party payment provider to your application. It indicates a change in a payment’s status. +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. -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. +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. *** -## getWebhookActionAndData Method +## How is Tax-Inclusive Pricing Set? -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. +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: -Medusa implements a webhook listener route at the `/hooks/payment/[identifier]_[provider]` API route, where: +- `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`. -- `[identifier]` is the `identifier` static property defined in the payment provider. For example, `stripe`. -- `[provider]` is the ID of the provider. For example, `stripe`. +Only `region_id` and `currency_code` are supported as an `attribute` at the moment. -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`. +The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context. -Use that webhook listener in your third-party payment provider's configurations. +For example: - +```json +{ + "attribute": "currency_code", + "value": "USD", + "is_tax_inclusive": true, +} +``` -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. +In this example, tax-inclusivity is enabled for the `USD` currency code. -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 +## Tax-Inclusive Pricing in Price Calculation -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. +### 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. -# Order Exchange +To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context. -In this document, you’ll learn about order exchanges. +### Returned Tax Properties -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard. +The `calculatePrices` method returns two properties related to tax-inclusivity: -## What is an Exchange? +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). -An exchange is the replacement of an item that the customer ordered with another. +- `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 merchant creates the exchange, specifying the items to be replaced and the new items to be sent. +A price is considered tax-inclusive if: -The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. +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. -*** +### Tax Context Precedence -## Returned and New Items +A region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if: -When the exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is created to handle receiving the items back from the customer. +- 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 -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). -The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer. +# Payment Collection -*** +In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module. -## Exchange Shipping Methods +## What's a Payment Collection? -An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). +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). -The shipping methods for the returned items are associated with the exchange'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). +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. *** -## Exchange Payment +## Multiple Payments -The `Exchange` data model has a `difference_due` property that stores the outstanding amount. +The payment collection supports multiple payment sessions and payments. -|Condition|Result| -|---|---|---| -|\`difference\_due \< 0\`|Merchant owes the customer a refund of the | -|\`difference\_due > 0\`|Merchant requires additional payment from the customer of the | -|\`difference\_due = 0\`|No payment processing is required.| +You can use this to accept payments in increments or split payments across payment providers. -Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). + *** -## How Exchanges Impact an Order’s Version - -When an exchange is confirmed, the order’s version is incremented. - +## Usage with the Cart Module -# Order Claim +The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment. -In this document, you’ll learn about order claims. +During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing. -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. +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). -## What is a Claim? + -When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. -The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. +# Links between Payment Module and Other Modules -*** +This document showcases the module links defined between the Payment Module and other commerce modules. -## Claim Type +## Summary -The `Claim` data model has a `type` property whose value indicates the type of the claim: +The Payment Module has the following links to other modules: -- `refund`: the items are returned, and the customer is refunded. -- `replace`: the items are returned, and the customer receives new items. +- [`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). *** -## Old and Replacement Items +## Cart Module -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. +The Cart Module provides cart-related features, but not payment processing. -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). +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. -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). +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 -## Claim Shipping Methods +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`: -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). +### query.graph -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). +```ts +const { data: paymentCollections } = await query.graph({ + entity: "payment_collection", + fields: [ + "cart.*", + ], +}) -*** +// paymentCollections.cart +``` -## Claim Refund +### useQueryGraphStep -If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim. +// ... -*** +const { data: paymentCollections } = useQueryGraphStep({ + entity: "payment_collection", + fields: [ + "cart.*", + ], +}) -## How Claims Impact an Order’s Version +// paymentCollections.cart +``` -When a claim is confirmed, the order’s version is incremented. +### Manage with Link +To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -# Order Return +### link.create -In this document, you’ll learn about order returns. +```ts +import { Modules } from "@medusajs/framework/utils" -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard. +// ... -## What is a Return? +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` -A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md). +### createRemoteLinkStep -A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow. +```ts +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - +// ... -Once the merchant receives the returned items, they mark the return as received. +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` *** -## Returned Items - -The items to be returned are represented by the [ReturnItem data model](references/order/models/ReturnItem). - -The `ReturnItem` model has two properties storing the item's quantity: - -1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity. -2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity. +## 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. -## Return Shipping Methods +This link is available starting from Medusa `v2.5.0`. -A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](references/order/models/OrderShippingMethod). +### Retrieve with Query -In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled. +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 -## Refund Payment +```ts +const { data: accountHolders } = await query.graph({ + entity: "account_holder", + fields: [ + "customer.*", + ], +}) -The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer. +// accountHolders.customer +``` -The [OrderTransaction data model](references/order/models/OrderTransaction) represents the refunds made for the return. +### useQueryGraphStep -*** +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -## Returns in Exchanges and Claims +// ... -When a merchant creates an exchange or a claim, it includes returning items from the customer. +const { data: accountHolders } = useQueryGraphStep({ + entity: "account_holder", + fields: [ + "customer.*", + ], +}) -The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for. +// accountHolders.customer +``` -*** +### Manage with Link -## How Returns Impact an Order’s Version +To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -The order’s version is incremented when: +### link.create -1. A return is requested. -2. A return is marked as received. +```ts +import { Modules } from "@medusajs/framework/utils" +// ... -# Links between Pricing Module and Other Modules +await link.create({ + [Modules.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_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.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", + }, +}) +``` *** -## Fulfillment Module +## Order 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. +An order's payment details are stored in a payment collection. This also applies for claims and exchanges. -Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. +So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models. - + ### 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 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: [ - "shipping_option.*", + "order.*", ], }) -// priceSets.shipping_option +// paymentCollections.order ``` ### useQueryGraphStep @@ -22782,19 +22526,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", +const { data: paymentCollections } = useQueryGraphStep({ + entity: "payment_collection", fields: [ - "shipping_option.*", + "order.*", ], }) -// priceSets.shipping_option +// paymentCollections.order ``` ### 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 payment collections of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -22804,11 +22548,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", }, }) ``` @@ -22822,44 +22566,40 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.PAYMENT]: { + payment_collection_id: "paycol_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. +## Region Module - +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. -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. +This increases the flexibility of your store. For example, you only show during checkout the payment providers associated with the cart's region. ### 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 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: priceSets } = await query.graph({ - entity: "price_set", +const { data: paymentProviders } = await query.graph({ + entity: "payment_provider", fields: [ - "variant.*", + "regions.*", ], }) -// priceSets.variant +// paymentProviders.regions ``` ### useQueryGraphStep @@ -22869,19 +22609,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", +const { data: paymentProviders } = useQueryGraphStep({ + entity: "payment_provider", fields: [ - "variant.*", + "regions.*", ], }) -// priceSets.variant +// paymentProviders.regions ``` ### 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 providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -22891,11 +22631,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", + [Modules.REGION]: { + region_id: "reg_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", }, }) ``` @@ -22909,329 +22649,391 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", + [Modules.REGION]: { + region_id: "reg_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", }, }) ``` -# Pricing Concepts +# Payment Module Options -In this document, you’ll learn about the main concepts in the Pricing Module. +In this document, you'll learn about the options of the Payment Module. -## Price Set +## All Module Options -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). +|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|-| -Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). +*** - +## providers Option -*** +The `providers` option is an array of payment module providers. -## Price List +When the Medusa application starts, these providers are registered and can be used to process payments. -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. +For example: -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 title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" -Its associated prices are represented by the `Price` data model. +// ... +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/payment", + options: { + providers: [ + { + resolve: "@medusajs/medusa/payment-stripe", + id: "stripe", + options: { + // ... + }, + }, + ], + }, + }, + ], +}) +``` -# Prices Calculation +The `providers` option is an array of objects that accept the following properties: -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. +- `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. -## calculatePrices Method -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. +# Accept Payment Flow -It returns a price object with the best matching price for each price set. +In this document, you’ll learn how to implement an accept-payment flow using workflows or the Payment Module's main service. -### Calculation Context +It's highly recommended to use Medusa's workflows to implement this flow. Use the Payment Module's main service for more complex cases. -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 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). -For example: +## Flow Overview -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSetId] }, - { - context: { - currency_code: currencyCode, - region_id: "reg_123", - }, - } -) -``` + -In this example, you retrieve the prices in a price set for the specified currency code and region ID. +*** -### Returned Price Object +## 1. Create a Payment Collection -For each price set, the `calculatePrices` method selects two prices: +A payment collection holds all details related to a resource’s payment operations. So, you start off by creating a payment collection. -- 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. +For example: -Both prices are returned in an object that has the following properties: +### Using Workflow -- 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 +import { createPaymentCollectionForCartWorkflow } from "@medusajs/medusa/core-flows" - - id: (\`string\`) The ID of the price. +// ... - - price\_list\_id: (\`string\`) The ID of the associated price list. +await createPaymentCollectionForCartWorkflow(req.scope) + .run({ + input: { + cart_id: "cart_123", + }, + }) +``` - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. +### Using Service - - min\_quantity: (\`number\`) The price's min quantity condition. +```ts +const paymentCollection = + await paymentModuleService.createPaymentCollections({ + currency_code: "usd", + amount: 5000, + }) +``` - - 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. +## 2. Create Payment Sessions - - price\_list\_id: (\`string\`) The ID of the associated price list. +The payment collection has one or more payment sessions, each being a payment amount to be authorized by a payment provider. - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. +So, after creating the payment collection, create at least one payment session for a provider. - - min\_quantity: (\`number\`) The price's min quantity condition. +For example: - - max\_quantity: (\`number\`) The price's max quantity condition. +### Using Workflow -*** +```ts +import { createPaymentSessionsWorkflow } from "@medusajs/medusa/core-flows" -## Examples +// ... -Consider the following price set: +const { result: paymentSesion } = await createPaymentSessionsWorkflow(req.scope) + .run({ + input: { + payment_collection_id: "paycol_123", + provider_id: "stripe", + }, + }) +``` + +### Using Service ```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", - }, - }, +const paymentSession = + await paymentModuleService.createPaymentSession( + paymentCollection.id, { - amount: 500, - currency_code: "EUR", - rules: { - city: "warsaw", - region_id: "reg_123", + provider_id: "stripe", + currency_code: "usd", + amount: 5000, + data: { + // any necessary data for the + // payment provider }, - }, - ], -}) + } + ) ``` -### Default Price Selection +*** -### Code +## 3. Authorize Payment Session + +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. + +For example: + +### Using Step ```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR" - } - } -) -``` +import { authorizePaymentSessionStep } from "@medusajs/medusa/core-flows" -### Result +// ... -### Calculate Prices with Rules +authorizePaymentSessionStep({ + id: "payses_123", + context: {}, +}) +``` -### Code +### Using Service ```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR", - region_id: "reg_123", - city: "krakow" - } - } -) +const payment = authorizePaymentSessionStep({ + id: "payses_123", + context: {}, +}) ``` -### Result +When the payment authorization is successful, a payment is created and returned. -### Price Selection with Price List +### Handling Additional Action -### Code +If you used the `authorizePaymentSessionStep`, you don't need to implement this logic as it's implemented in the step. + +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. + +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. + +For example: ```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, - }, - ], -}]); +try { + const payment = + await paymentModuleService.authorizePaymentSession( + paymentSession.id, + {} + ) +} catch (e) { + // retrieve the payment session again + const updatedPaymentSession = ( + await paymentModuleService.listPaymentSessions({ + id: [paymentSession.id], + }) + )[0] -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR", - region_id: "PL", - city: "krakow" - } + if (updatedPaymentSession.status === "requires_more") { + // TODO perform required action + // TODO authorize payment again. } -) +} ``` -### Result +*** +## 4. Payment Flow Complete -# Price Rules +The payment flow is complete once the payment session is authorized and the payment is created. -In this document, you'll learn about price rules for price sets and price lists. +You can then: -## Price Rule +- 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). -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). +Some payment providers allow capturing the payment automatically once it’s authorized. In that case, you don’t need to do it manually. -The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price. -For exmaple, you create a price restricted to `10557` zip codes. +# Payment - +In this document, you’ll learn what a payment is and how it's created, captured, and refunded. -A price can have multiple price rules. +## What's a Payment? -For example, a price can be restricted by a region and a zip 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: + +- 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. *** -## Price List Rules +## Capture Payments -Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md). +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. -The `rules_count` property of a `PriceList` indicates how many rules are applied to it. +The payment can also be captured incrementally, each time a capture record is created for that amount. - + +*** -# Tax-Inclusive Pricing +## Refund Payments -In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. +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. -## What is Tax-Inclusive Pricing? +A payment can be refunded multiple times, and each time a refund record is created. -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. + +# Payment Module Provider + +In this document, you’ll learn what a payment module provider is. + +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. + +## What's a Payment Module Provider? + +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. + +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. + +### List of Payment Module Providers + +- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md) *** -## How is Tax-Inclusive Pricing Set? +## System Payment Provider -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: +The Payment Module provides a `system` payment provider that acts as a placeholder payment provider. -- `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`. +It doesn’t handle payment processing and delegates that to the merchant. It acts similarly to a cash-on-delivery (COD) payment method. -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. +## How are Payment Providers Created? -For example: +A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`. -```json -{ - "attribute": "currency_code", - "value": "USD", - "is_tax_inclusive": true, -} -``` +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. -In this example, tax-inclusivity is enabled for the `USD` currency code. +*** + +## Configure Payment Providers + +The Payment Module accepts a `providers` option that allows you to register providers in your application. + +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). *** -## Tax-Inclusive Pricing in Price Calculation +## PaymentProvider Data Model -### Tax Context +When the Medusa application starts and registers the payment providers, it also creates a record of the `PaymentProvider` data model if none exists. -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. +This data model is used to reference a payment provider and determine whether it’s installed in the application. -To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context. -### Returned Tax Properties +# Payment Session -The `calculatePrices` method returns two properties related to tax-inclusivity: +In this document, you’ll learn what a payment session is. -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). +## What's a Payment Session? -- `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 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. -A price is considered tax-inclusive if: +A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. -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. + -### Tax Context Precedence +*** -A region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if: +## data Property -- 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 providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data. + +For example, the customer's ID in Stripe is stored in the `data` property. + +*** + +## 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. + + +# 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. + + + +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 + +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. # Links between Product Module and Other Modules @@ -23795,6 +23597,204 @@ 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.||| +# Customer Accounts + +In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. + +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. + +## `has_account` Property + +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. + +When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. + +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`. + +*** + +## Email Uniqueness + +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. + +So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. + + +# Links between Customer Module and Other Modules + +This document showcases the module links defined between the Customer Module and other commerce modules. + +## Summary + +The Customer 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. + +- [`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). + +*** + +## 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`. + +### Retrieve with Query + +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`: + +### query.graph + +```ts +const { data: customers } = await query.graph({ + entity: "customer", + fields: [ + "account_holder.*", + ], +}) + +// customers.account_holder +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: [ + "account_holder.*", + ], +}) + +// customers.account_holder +``` + +### Manage with Link + +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 + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", + }, +}) +``` + +*** + +## Cart Module + +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. + +### Retrieve with Query + +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 + +```ts +const { data: customers } = await query.graph({ + entity: "customer", + fields: [ + "carts.*", + ], +}) + +// customers.carts +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: [ + "carts.*", + ], +}) + +// customers.carts +``` + +*** + +## Order Module + +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. + +### Retrieve with Query + +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`: + +### query.graph + +```ts +const { data: customers } = await query.graph({ + entity: "customer", + fields: [ + "orders.*", + ], +}) + +// customers.orders +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: [ + "orders.*", + ], +}) + +// customers.orders +``` + + # Links between Region Module and Other Modules This document showcases the module links defined between the Region Module and other commerce modules. @@ -23973,317 +23973,278 @@ createRemoteLinkStep({ ``` -# 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. +# Application Method -## Publishable API Keys with Sales Channels +In this document, you'll learn what an application method is. -A publishable API key, provided by the API Key Module, is a client key scoped to one or more sales channels. +## What is an Application Method? -When sending a request to a Store API route, you must pass a publishable API key in the header of the request: +The [ApplicationMethod data model](https://docs.medusajs.com/references/promotion/models/ApplicationMethod/index.html.md) defines how a promotion is applied: -```bash -curl http://localhost:9000/store/products \ - x-publishable-api-key: {your_publishable_api_key} -``` +|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?| -The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used. +## 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. -## How to Create a Publishable API Key? +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. -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). + +In this example, the promotion is only applied on products in the cart having the SKU `SHIRT`. -# Links between Sales Channel Module and Other Modules +*** -This document showcases the module links defined between the Sales Channel Module and other commerce modules. +## Buy Promotion Rules -## Summary +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 Sales Channel Module has the following links to other modules: +The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation. -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). +In this example, the cart must have two products with the SKU `SHIRT` for the promotion to be applied. -*** -## API Key Module +# Promotion Actions -A publishable API key allows you to easily specify the sales channel scope in a client request. +In this document, you’ll learn about promotion actions and how they’re computed using the [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). -Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. +## computeActions Method - +The Promotion Module's main service has a [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md) that returns an array of actions to perform on a cart when one or more promotions are applied. -### Retrieve with Query +Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` property indicating the type of action. -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 +## Action Types -```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "publishable_api_keys.*", - ], -}) +### `addItemAdjustment` Action -// salesChannels.publishable_api_keys -``` +The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount. -### useQueryGraphStep +This action has the following format: ```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +export interface AddItemAdjustmentAction { + action: "addItemAdjustment" + item_id: string + amount: number + code: string + description?: string +} +``` -// ... +This action means that a new record should be created of the `LineItemAdjustment` data model in the Cart Module, or `OrderLineItemAdjustment` data model in the Order Module. -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "publishable_api_keys.*", - ], -}) +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties. -// salesChannels.publishable_api_keys -``` +### `removeItemAdjustment` Action -### Manage with Link +The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount. -To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter. -### link.create +This action has the following format: ```ts -import { Modules } from "@medusajs/framework/utils" +export interface RemoveItemAdjustmentAction { + action: "removeItemAdjustment" + adjustment_id: string + description?: string + code: string +} +``` -// ... +This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property. -await link.create({ - [Modules.API_KEY]: { - api_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties. -### createRemoteLinkStep +### `addShippingMethodAdjustment` Action -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +The `addShippingMethodAdjustment` action indicates that an adjustment must be made on a shipping method. For example, make the shipping method free. -// ... +This action has the following format: -createRemoteLinkStep({ - [Modules.API_KEY]: { - api_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) +```ts +export interface AddShippingMethodAdjustment { + action: "addShippingMethodAdjustment" + shipping_method_id: string + amount: number + code: string + description?: string +} ``` -*** +This action means that a new record should be created of the `ShippingMethodAdjustment` data model in the Cart Module, or `OrderShippingMethodAdjustment` data model in the Order Module. -## Cart Module +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties. -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. +### `removeShippingMethodAdjustment` Action -### Retrieve with Query +The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount. -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`: +The `computeActions` method accepts any previous shipping method adjustments in the `shipping_methods` property of the second parameter. -### query.graph +This action has the following format: ```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "carts.*", - ], -}) +export interface RemoveShippingMethodAdjustment { + action: "removeShippingMethodAdjustment" + adjustment_id: string + code: string +} +``` + +When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property. + +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties. + +### `campaignBudgetExceeded` Action + +When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded. + +This action has the following format: -// salesChannels.carts +```ts +export interface CampaignBudgetExceededAction { + action: "campaignBudgetExceeded" + code: string +} ``` -### useQueryGraphStep +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -// ... +# Campaign -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "carts.*", - ], -}) +In this document, you'll learn about campaigns. -// salesChannels.carts -``` +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? + +A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines promotions under the same conditions, such as start and end dates. + + *** -## Order Module +## Campaign Limits -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. +Each campaign has a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times the promotion can be used. -### Retrieve with Query +There are two types of budgets: -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`: +- `spend`: An amount that, when crossed, the promotion becomes unusable. For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied. +- `usage`: The number of times that a promotion can be used. For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied. -### query.graph + -```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "orders.*", - ], -}) -// salesChannels.orders -``` +# Promotion Concepts -### useQueryGraphStep +In this document, you’ll learn about the main promotion and rule concepts in the Promotion Module. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. -// ... +## What is a Promotion? -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "orders.*", - ], -}) +A promotion, represented by the [Promotion data model](https://docs.medusajs.com/references/promotion/models/Promotion/index.html.md), is a discount that can be applied on cart items, shipping methods, or entire orders. -// salesChannels.orders -``` +A promotion has two types: -*** +- `standard`: A standard promotion with rules. +- `buyget`: “A buy X get Y” promotion with rules. -## Product Module +|\`standard\`|\`buyget\`| +|---|---| +|A coupon code that gives customers 10% off their entire order.|Buy two shirts and get another for free.| +|A coupon code that gives customers $15 off any shirt in their order.|Buy two shirts and get 10% off the entire order.| +|A discount applied automatically for VIP customers that removes 10% off their shipping method’s amount.|Spend $100 and get free shipping.| -A product has different availability for different sales channels. Medusa defines a link between the `Product` and the `SalesChannel` data models. +The Medusa Admin UI may not provide a way to create each of these promotion examples. However, they are supported by the Promotion Module and Medusa's workflows and API routes. - +*** -A product can be available in more than one sales channel. You can retrieve only the products of a sales channel. +## PromotionRule -### Retrieve with Query +A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](https://docs.medusajs.com/references/promotion/models/PromotionRule/index.html.md). -To retrieve the products of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: +For example, you can create a promotion that only customers of the `VIP` customer group can use. -### query.graph + -```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "products.*", - ], -}) +A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. -// salesChannels.products -``` +For example, `customer_group_id`. Its value is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values. -### useQueryGraphStep +When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value. -// ... +*** -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "products.*", - ], -}) +## Flexible Rules -// salesChannels.products -``` +The `PromotionRule`'s `operator` property adds more flexibility to the rule’s condition rather than simple equality (`eq`). -### Manage with Link +For example, to restrict the promotion to only `VIP` and `B2B` customer groups: -To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +- Add a `PromotionRule` record with its `attribute` property set to `customer_group_id` and `operator` property to `in`. +- Add two `PromotionRuleValue` records associated with the rule: one with the value `VIP` and the other `B2B`. -### link.create + -```ts -import { Modules } from "@medusajs/framework/utils" +In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion. -// ... -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` +# Links between Promotion Module and Other Modules -### createRemoteLinkStep +This document showcases the module links defined between the Promotion Module and other commerce modules. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +## Summary -// ... +The Promotion Module has the following links to other modules: -createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` +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). *** -## Stock Location Module +## Cart Module -A stock location is associated with a sales channel. This scopes inventory quantities associated with that stock location by the associated sales channel. +A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` and `Promotion` data models. -Medusa defines a link between the `SalesChannel` and `StockLocation` data models. + - +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. ### Retrieve with Query -To retrieve the stock locations of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: +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`. ### query.graph ```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", +const { data: promotions } = await query.graph({ + entity: "promotion", fields: [ - "stock_locations.*", + "carts.*", ], }) -// salesChannels.stock_locations +// promotions.carts ``` ### useQueryGraphStep @@ -24293,19 +24254,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", +const { data: promotions } = useQueryGraphStep({ + entity: "promotion", fields: [ - "stock_locations.*", + "carts.*", ], }) -// salesChannels.stock_locations +// promotions.carts ``` ### 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): +To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -24315,11 +24276,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", + [Modules.PROMOTION]: { + promotion_id: "promo_123", }, }) ``` @@ -24333,62 +24294,38 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", + [Modules.PROMOTION]: { + promotion_id: "promo_123", }, }) ``` - -# Links between Stock Location Module and Other Modules - -This document showcases the module links defined between the Stock Location Module and other commerce modules. - -## Summary - -The Stock Location 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). - *** -## Fulfillment Module - -A fulfillment set can be conditioned to a specific stock location. - -Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. - - +## Order Module -Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. +An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. - + ### 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 orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: ### query.graph ```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", +const { data: promotions } = await query.graph({ + entity: "promotion", fields: [ - "fulfillment_sets.*", + "orders.*", ], }) -// stockLocations.fulfillment_sets +// promotions.orders ``` ### useQueryGraphStep @@ -24398,19 +24335,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", +const { data: promotions } = useQueryGraphStep({ + entity: "promotion", fields: [ - "fulfillment_sets.*", + "orders.*", ], }) -// stockLocations.fulfillment_sets +// promotions.orders ``` ### 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): +To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -24420,11 +24357,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", + [Modules.PROMOTION]: { + promotion_id: "promo_123", }, }) ``` @@ -24438,80 +24375,81 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", + [Modules.PROMOTION]: { + promotion_id: "promo_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. +# Publishable API Keys with Sales Channels -### Retrieve with Query +In this document, you’ll learn what publishable API keys are and how to use them with sales channels. -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`: +## Publishable API Keys with Sales Channels -### query.graph +A publishable API key, provided by the API Key Module, is a client key scoped to one or more sales channels. -```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: [ - "inventory_levels.*", - ], -}) +When sending a request to a Store API route, you must pass a publishable API key in the header of the request: -// stockLocations.inventory_levels +```bash +curl http://localhost:9000/store/products \ + x-publishable-api-key: {your_publishable_api_key} ``` -### useQueryGraphStep +The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +*** -// ... +## How to Create a Publishable API Key? -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: [ - "inventory_levels.*", - ], -}) +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). -// stockLocations.inventory_levels -``` + +# 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). *** -## Sales Channel Module +## API Key Module -A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel. +A publishable API key allows you to easily specify the sales channel scope in a client request. -Medusa defines a link between the `SalesChannel` and `StockLocation` data models. +Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. - + ### 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`: +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: [ - "sales_channels.*", + "publishable_api_keys.*", ], }) -// stockLocations.sales_channels +// salesChannels.publishable_api_keys ``` ### useQueryGraphStep @@ -24521,19 +24459,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", fields: [ - "sales_channels.*", + "publishable_api_keys.*", ], }) -// stockLocations.sales_channels +// salesChannels.publishable_api_keys ``` ### 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): +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 @@ -24543,12 +24481,12 @@ 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", }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, }) ``` @@ -24561,305 +24499,203 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` - - -# Stock Location Concepts - -In this document, you’ll learn about the main concepts in the Stock Location Module. - -## Stock Location - -A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. - -Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. - -*** - -## StockLocationAddress - -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. - - -# 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. - - - -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. - - - -In this example, the cart must have two products with the SKU `SHIRT` for the promotion to be applied. - - -# Promotion Actions - -In this document, you’ll learn about promotion actions and how they’re computed using the [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). - -## computeActions Method - -The Promotion Module's main service has a [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md) that returns an array of actions to perform on a cart when one or more promotions are applied. - -Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` property indicating the type of action. - -*** - -## Action Types - -### `addItemAdjustment` Action - -The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount. - -This action has the following format: - -```ts -export interface AddItemAdjustmentAction { - action: "addItemAdjustment" - item_id: string - amount: number - code: string - description?: string -} -``` - -This action means that a new record should be created of the `LineItemAdjustment` data model in the Cart Module, or `OrderLineItemAdjustment` data model in the Order Module. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties. - -### `removeItemAdjustment` Action - -The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount. - -The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter. - -This action has the following format: - -```ts -export interface RemoveItemAdjustmentAction { - action: "removeItemAdjustment" - adjustment_id: string - description?: string - code: string -} -``` - -This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties. - -### `addShippingMethodAdjustment` Action - -The `addShippingMethodAdjustment` action indicates that an adjustment must be made on a shipping method. For example, make the shipping method free. - -This action has the following format: - -```ts -export interface AddShippingMethodAdjustment { - action: "addShippingMethodAdjustment" - shipping_method_id: string - amount: number - code: string - description?: string -} -``` - -This action means that a new record should be created of the `ShippingMethodAdjustment` data model in the Cart Module, or `OrderShippingMethodAdjustment` data model in the Order Module. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties. - -### `removeShippingMethodAdjustment` Action - -The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount. - -The `computeActions` method accepts any previous shipping method adjustments in the `shipping_methods` property of the second parameter. - -This action has the following format: - -```ts -export interface RemoveShippingMethodAdjustment { - action: "removeShippingMethodAdjustment" - adjustment_id: string - code: string -} + [Modules.API_KEY]: { + api_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) ``` -When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties. +*** -### `campaignBudgetExceeded` Action +## Cart Module -When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded. +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. -This action has the following format: +### Retrieve with Query -```ts -export interface CampaignBudgetExceededAction { - action: "campaignBudgetExceeded" - code: string -} -``` +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`: -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties. +### query.graph +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "carts.*", + ], +}) -# Campaign +// salesChannels.carts +``` -In this document, you'll learn about campaigns. +### useQueryGraphStep -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. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -## What is a Campaign? +// ... -A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines promotions under the same conditions, such as start and end dates. +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", + fields: [ + "carts.*", + ], +}) - +// salesChannels.carts +``` *** -## Campaign Limits - -Each campaign has a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times the promotion can be used. +## Order Module -There are two types of budgets: +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. -- `spend`: An amount that, when crossed, the promotion becomes unusable. For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied. -- `usage`: The number of times that a promotion can be used. For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied. +### 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`: +### query.graph -# Promotion Concepts +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "orders.*", + ], +}) -In this document, you’ll learn about the main promotion and rule concepts in the Promotion Module. +// salesChannels.orders +``` -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. +### useQueryGraphStep -## What is a Promotion? +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -A promotion, represented by the [Promotion data model](https://docs.medusajs.com/references/promotion/models/Promotion/index.html.md), is a discount that can be applied on cart items, shipping methods, or entire orders. +// ... -A promotion has two types: +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", + fields: [ + "orders.*", + ], +}) -- `standard`: A standard promotion with rules. -- `buyget`: “A buy X get Y” promotion with rules. +// salesChannels.orders +``` -|\`standard\`|\`buyget\`| -|---|---| -|A coupon code that gives customers 10% off their entire order.|Buy two shirts and get another for free.| -|A coupon code that gives customers $15 off any shirt in their order.|Buy two shirts and get 10% off the entire order.| -|A discount applied automatically for VIP customers that removes 10% off their shipping method’s amount.|Spend $100 and get free shipping.| +*** -The Medusa Admin UI may not provide a way to create each of these promotion examples. However, they are supported by the Promotion Module and Medusa's workflows and API routes. +## Product Module -*** +A product has different availability for different sales channels. Medusa defines a link between the `Product` and the `SalesChannel` data models. -## PromotionRule + -A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](https://docs.medusajs.com/references/promotion/models/PromotionRule/index.html.md). +A product can be available in more than one sales channel. You can retrieve only the products of a sales channel. -For example, you can create a promotion that only customers of the `VIP` customer group can use. +### Retrieve with Query - +To retrieve the products of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: -A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. +### query.graph -For example, `customer_group_id`. Its value is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values. +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "products.*", + ], +}) -When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself. +// salesChannels.products +``` -For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value. +### useQueryGraphStep -*** +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -## Flexible Rules +// ... -The `PromotionRule`'s `operator` property adds more flexibility to the rule’s condition rather than simple equality (`eq`). +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", + fields: [ + "products.*", + ], +}) -For example, to restrict the promotion to only `VIP` and `B2B` customer groups: +// salesChannels.products +``` -- Add a `PromotionRule` record with its `attribute` property set to `customer_group_id` and `operator` property to `in`. -- Add two `PromotionRuleValue` records associated with the rule: one with the value `VIP` and the other `B2B`. +### Manage with Link - +To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion. +### link.create +```ts +import { Modules } from "@medusajs/framework/utils" -# Links between Promotion Module and Other Modules +// ... -This document showcases the module links defined between the Promotion Module and other commerce modules. +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` -## Summary +### createRemoteLinkStep -The Promotion Module has the following links to other modules: +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -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). +createRemoteLinkStep({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` *** -## Cart Module +## Stock Location Module -A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` and `Promotion` data models. +A stock location is associated with a sales channel. This scopes inventory quantities associated with that stock location by the associated sales channel. - +Medusa defines a link between the `SalesChannel` and `StockLocation` data models. -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. + ### 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 stock locations of a sales channel 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: promotions } = await query.graph({ - entity: "promotion", +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", fields: [ - "carts.*", + "stock_locations.*", ], }) -// promotions.carts +// salesChannels.stock_locations ``` ### useQueryGraphStep @@ -24869,19 +24705,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: promotions } = useQueryGraphStep({ - entity: "promotion", +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", fields: [ - "carts.*", + "stock_locations.*", ], }) -// promotions.carts +// salesChannels.stock_locations ``` ### 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): +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 @@ -24891,11 +24727,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.CART]: { - cart_id: "cart_123", + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", }, }) ``` @@ -24909,38 +24745,62 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", }, }) ``` + +# Links between Stock Location Module and Other Modules + +This document showcases the module links defined between the Stock Location Module and other commerce modules. + +## Summary + +The Stock Location 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). + *** -## Order Module +## Fulfillment Module -An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. +A fulfillment set can be conditioned to a specific stock location. - +Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. + + + +Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. + + ### Retrieve with Query -To retrieve the orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: +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: promotions } = await query.graph({ - entity: "promotion", +const { data: stockLocations } = await query.graph({ + entity: "stock_location", fields: [ - "orders.*", + "fulfillment_sets.*", ], }) -// promotions.orders +// stockLocations.fulfillment_sets ``` ### useQueryGraphStep @@ -24950,19 +24810,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: promotions } = useQueryGraphStep({ - entity: "promotion", +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", fields: [ - "orders.*", + "fulfillment_sets.*", ], }) -// promotions.orders +// stockLocations.fulfillment_sets ``` ### Manage with Link -To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +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 @@ -24972,11 +24832,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", }, }) ``` @@ -24990,51 +24850,80 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", }, }) ``` +*** -# Links between Store Module and Other Modules +## Inventory Module -This document showcases the module links defined between the Store Module and other commerce modules. +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. -## Summary +### Retrieve with Query -The Store Module has the following links to other modules: +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`: -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +### query.graph -- [`Currency` data model \<> `Currency` data model of Currency Module](#currency-module). (Read-only). +```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 +``` *** -## Currency Module +## Sales Channel 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. +A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel. -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. +Medusa defines a link between the `SalesChannel` and `StockLocation` data models. + + ### 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 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: stores } = await query.graph({ - entity: "store", +const { data: stockLocations } = await query.graph({ + entity: "stock_location", fields: [ - "supported_currencies.currency.*", + "sales_channels.*", ], }) -// stores.supported_currencies +// stockLocations.sales_channels ``` ### useQueryGraphStep @@ -25044,17 +24933,73 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stores } = useQueryGraphStep({ - entity: "store", +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", fields: [ - "supported_currencies.currency.*", + "sales_channels.*", ], }) -// stores.supported_currencies +// 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", + }, +}) ``` +# Stock Location Concepts + +In this document, you’ll learn about the main concepts in the Stock Location Module. + +## Stock Location + +A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. + +Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. + +*** + +## StockLocationAddress + +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. + + # User Module Options In this document, you'll learn about the options of the User Module. @@ -25172,6 +25117,102 @@ if (!count) { ``` +# Tax Module Options + +In this document, you'll learn about the options of the Tax Module. + +## providers + +The `providers` option is an array of either tax module providers or path to a file that defines a tax provider. + +When the Medusa application starts, these providers are registered and can be used to retrieve tax lines. + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/tax", + options: { + providers: [ + { + resolve: "./path/to/my-provider", + id: "my-provider", + options: { + // ... + }, + }, + ], + }, + }, + ], +}) +``` + +The objects in the array accept the following properties: + +- `resolve`: A string indicating the package name of the module provider or the path to it. +- `id`: A string indicating the provider's unique name or ID. +- `options`: An optional object of the module provider's options. + + +# 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. + + + +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 Region + +In this document, you’ll learn about tax regions and how to use them with the Region Module. + +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. + +## 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. + +Tax regions can inherit settings and rules from a parent tax region. + +Each tax region has tax rules and a tax provider. + + # Tax Calculation with the Tax Provider In this document, you’ll learn how tax lines are calculated and what a tax provider is. @@ -25249,100 +25290,59 @@ TODO add once tax provider guide is updated + add module providers match other m Refer to [this guide](/modules/tax/provider) to learn more about creating a tax provider. */} -# Tax Module Options - -In this document, you'll learn about the options of the Tax Module. - -## providers - -The `providers` option is an array of either tax module providers or path to a file that defines a tax provider. - -When the Medusa application starts, these providers are registered and can be used to retrieve tax lines. - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/tax", - options: { - providers: [ - { - resolve: "./path/to/my-provider", - id: "my-provider", - options: { - // ... - }, - }, - ], - }, - }, - ], -}) -``` - -The objects in the array accept the following properties: - -- `resolve`: A string indicating the package name of the module provider or the path to it. -- `id`: A string indicating the provider's unique name or ID. -- `options`: An optional object of the module provider's options. - - -# 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? +# Links between Store Module and Other Modules -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. +This document showcases the module links defined between the Store Module and other commerce modules. -Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. +## Summary -### Combinable Tax Rates +The Store Module has the following links to other modules: -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`. +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -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. +- [`Currency` data model \<> `Currency` data model of Currency Module](#currency-module). (Read-only). *** -## 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. +## Currency 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. -These two properties of the data model identify the rule’s target: +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. -- `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. +### Retrieve with Query -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. +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`: +### query.graph -# Tax Region +```ts +const { data: stores } = await query.graph({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) -In this document, you’ll learn about tax regions and how to use them with the Region Module. +// stores.supported_currencies +``` -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. +### useQueryGraphStep -## What is a Tax Region? +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -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. +// ... -Tax regions can inherit settings and rules from a parent tax region. +const { data: stores } = useQueryGraphStep({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) -Each tax region has tax rules and a tax provider. +// stores.supported_currencies +``` # Emailpass Auth Module Provider @@ -25576,86 +25576,6 @@ 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). -# Get Product Variant Prices using Query - -In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). - -The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model. - -So, to retrieve data across the linked records of the two modules, you use Query. - -## Retrieve All Product Variant Prices - -To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`. - -For example: - -```ts highlights={[["6"]]} -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "*", - "variants.*", - "variants.prices.*", - ], - filters: { - id: [ - "prod_123", - ], - }, -}) -``` - -Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). - -*** - -## Retrieve Calculated Price for a Context - -The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code. - -Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md). - -To retrieve calculated prices of variants based on a context, retrieve the products using Query and: - -- Pass `variants.calculated_price.*` in the `fields` property. -- Pass a `context` property in the object parameter. Its value is an object of objects that sets the context for the retrieved fields. - -For example: - -```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]} -import { QueryContext } from "@medusajs/framework/utils" - -// ... - -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "*", - "variants.*", - "variants.calculated_price.*", - ], - filters: { - id: "prod_123", - }, - context: { - variants: { - calculated_price: QueryContext({ - region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS", - currency_code: "eur", - }), - }, - }, -}) -``` - -For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`. - -`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md). - -Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/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. @@ -25908,384 +25828,464 @@ taxLines.forEach((taxLine) => { taxLinesMap.set(variantId, []) } - taxLinesMap.get(variantId)?.push(taxLine) -}) -``` + taxLinesMap.get(variantId)?.push(taxLine) +}) +``` + +Notice that the variant's ID is stored in the `line_item_id` property of a tax line since tax lines are used for line items in a cart. + +Then, loop over the products and their variants to retrieve the prices with and without taxes: + +```ts highlights={calculateTaxHighlights} +// other imports... +import { + calculateAmountsWithTax, +} from "@medusajs/framework/utils" + +// ... +products.forEach((product) => { + product.variants?.forEach((variant) => { + if (!variant.calculated_price) { + return + } + + const taxLinesForVariant = taxLinesMap.get(variant.id) || [] + const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({ + taxLines: taxLinesForVariant, + amount: variant.calculated_price!.calculated_amount!, + includesTax: + variant.calculated_price!.is_calculated_price_tax_inclusive!, + }) + + // do something with prices... + }) +}) +``` + +For each product variant, you: + +1. Retrieve its tax lines from the `taxLinesMap`. +2. Calculate its prices with and without taxes using the `calculateAmountsWithTax` from the Medusa Framework. +3. The `calculateAmountsWithTax` function returns an object having two properties: + - `priceWithTax`: The variant's price with the taxes applied. + - `priceWithoutTax`: The variant's price without taxes applied. + + +# Get Product Variant Prices using Query + +In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). + +The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model. + +So, to retrieve data across the linked records of the two modules, you use Query. + +## Retrieve All Product Variant Prices + +To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`. + +For example: + +```ts highlights={[["6"]]} +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "*", + "variants.*", + "variants.prices.*", + ], + filters: { + id: [ + "prod_123", + ], + }, +}) +``` + +Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). + +*** + +## Retrieve Calculated Price for a Context + +The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code. + +Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md). + +To retrieve calculated prices of variants based on a context, retrieve the products using Query and: -Notice that the variant's ID is stored in the `line_item_id` property of a tax line since tax lines are used for line items in a cart. +- Pass `variants.calculated_price.*` in the `fields` property. +- Pass a `context` property in the object parameter. Its value is an object of objects that sets the context for the retrieved fields. -Then, loop over the products and their variants to retrieve the prices with and without taxes: +For example: -```ts highlights={calculateTaxHighlights} -// other imports... -import { - calculateAmountsWithTax, -} from "@medusajs/framework/utils" +```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]} +import { QueryContext } from "@medusajs/framework/utils" // ... -products.forEach((product) => { - product.variants?.forEach((variant) => { - if (!variant.calculated_price) { - return - } - - const taxLinesForVariant = taxLinesMap.get(variant.id) || [] - const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({ - taxLines: taxLinesForVariant, - amount: variant.calculated_price!.calculated_amount!, - includesTax: - variant.calculated_price!.is_calculated_price_tax_inclusive!, - }) - // do something with prices... - }) +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "*", + "variants.*", + "variants.calculated_price.*", + ], + filters: { + id: "prod_123", + }, + context: { + variants: { + calculated_price: QueryContext({ + region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS", + currency_code: "eur", + }), + }, + }, }) ``` -For each product variant, you: +For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`. -1. Retrieve its tax lines from the `taxLinesMap`. -2. Calculate its prices with and without taxes using the `calculateAmountsWithTax` from the Medusa Framework. -3. The `calculateAmountsWithTax` function returns an object having two properties: - - `priceWithTax`: The variant's price with the taxes applied. - - `priceWithoutTax`: The variant's price without taxes applied. +`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md). + +Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). ## Workflows - [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) - [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/index.html.md) -- [updateApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateApiKeysWorkflow/index.html.md) -- [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/index.html.md) - [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/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) - [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/index.html.md) -- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/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) -- [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) -- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/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) -- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/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) -- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) -- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) -- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md) -- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) - [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md) - [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md) +- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) - [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) - [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) +- [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md) - [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/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) -- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) - [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md) -- [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md) +- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) - [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) +- [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/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) -- [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md) - [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/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) -- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md) -- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md) +- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/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) +- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md) +- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md) +- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/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) +- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/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) +- [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) +- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) +- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/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) -- [createReturnFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnFulfillmentWorkflow/index.html.md) - [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md) -- [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/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) +- [createServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createServiceZonesWorkflow/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) - [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md) +- [markFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markFulfillmentAsDeliveredWorkflow/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) -- [markFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markFulfillmentAsDeliveredWorkflow/index.html.md) - [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/index.html.md) -- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md) - [updateFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateFulfillmentWorkflow/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) -- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) -- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/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) +- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md) +- [updateShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingProfilesWorkflow/index.html.md) +- [updateShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionsWorkflow/index.html.md) - [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/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) -- [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/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) +- [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md) - [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md) -- [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md) - [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/index.html.md) - [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md) +- [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/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) +- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) +- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md) - [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) -- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/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) -- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) -- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/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) -- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md) -- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md) -- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) -- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) -- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md) -- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md) -- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/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) -- [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/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) +- [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md) - [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md) -- [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md) +- [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/index.html.md) - [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md) -- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md) +- [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md) - [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/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) +- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md) - [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md) - [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md) - [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/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) - [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) +- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/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) - [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md) -- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/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) - [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md) +- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) - [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/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) -- [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/index.html.md) - [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/index.html.md) -- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/index.html.md) +- [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/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) - [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md) -- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) +- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/index.html.md) - [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/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) +- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) - [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md) -- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md) - [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md) +- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md) +- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) - [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md) - [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) - [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md) -- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) - [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) - [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/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) - [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md) - [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/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) - [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md) -- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/index.html.md) - [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/index.html.md) - [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/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) -- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) -- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md) - [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md) +- [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md) - [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md) -- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/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) - [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md) - [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/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) - [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/index.html.md) +- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/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) - [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md) -- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/index.html.md) - [deleteOrderPaymentCollections](https://docs.medusajs.com/references/medusa-workflows/deleteOrderPaymentCollections/index.html.md) - [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) - [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/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) - [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) +- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) - [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md) +- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md) - [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) - [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/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) -- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md) +- [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md) +- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/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) +- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md) - [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/index.html.md) -- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md) +- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md) - [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/index.html.md) -- [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md) - [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/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) - [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md) - [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md) +- [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md) - [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md) +- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) - [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md) - [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) -- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) - [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/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) - [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md) +- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/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) - [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md) -- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md) - [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md) - [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md) - [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md) -- [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md) - [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/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) +- [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/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) +- [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md) - [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/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) - [requestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnWorkflow/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) - [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/index.html.md) - [requestOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferValidationStep/index.html.md) -- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md) -- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md) -- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md) +- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md) +- [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/index.html.md) - [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md) +- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md) +- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md) +- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md) - [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md) -- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/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) +- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md) +- [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/index.html.md) +- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) - [updateExchangeAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemWorkflow/index.html.md) - [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md) - [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md) -- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md) - [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md) - [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md) -- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) - [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) +- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) +- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md) - [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md) -- [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/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) +- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) - [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md) - [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/index.html.md) -- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) +- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md) - [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md) -- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) -- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) - [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md) -- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md) +- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) +- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) - [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/index.html.md) - [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md) - [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md) +- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) +- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) +- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/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) +- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md) +- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/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) +- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/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) - [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/index.html.md) -- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md) - [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/index.html.md) -- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) +- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md) - [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) +- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) - [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) - [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md) -- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/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) - [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md) +- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md) +- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) - [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/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) -- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) -- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) +- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md) - [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md) +- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) +- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) - [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) -- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) - [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/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) +- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) +- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md) +- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/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) -- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) - [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md) -- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md) - [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/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) - [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) - [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md) - [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/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) -- [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) -- [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) - [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/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) -- [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md) - [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md) +- [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/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) - [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/index.html.md) -- [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/index.html.md) -- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/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) - [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/index.html.md) -- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md) - [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/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) +- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md) +- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/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) +- [deleteReservationsByLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsByLineItemsWorkflow/index.html.md) +- [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/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) +- [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) - [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) +- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md) - [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md) - [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md) -- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md) - [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/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) - [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/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) -- [updateStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStoresWorkflow/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) +- [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md) +- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/index.html.md) +- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md) +- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) +- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) +- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md) +- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md) - [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md) - [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) -- [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md) - [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md) +- [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/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) -- [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/index.html.md) - [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md) -- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) -- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) -- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/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) +- [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/index.html.md) ## Steps @@ -26294,168 +26294,126 @@ For each product variant, you: - [deleteApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteApiKeysStep/index.html.md) - [linkSalesChannelsToApiKeyStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkSalesChannelsToApiKeyStep/index.html.md) - [revokeApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/revokeApiKeysStep/index.html.md) -- [validateSalesChannelsExistStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateSalesChannelsExistStep/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) +- [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) +- [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) +- [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) +- [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md) +- [validatePresenceOfStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePresenceOfStep/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) - [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/index.html.md) +- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md) - [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/index.html.md) - [createLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemsStep/index.html.md) +- [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/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) - [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md) +- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/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) -- [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) - [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) -- [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md) +- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md) - [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) - [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) -- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md) - [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md) +- [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md) - [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md) +- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/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) -- [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/index.html.md) +- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/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) - [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) - [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md) -- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md) +- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md) +- [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md) - [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/index.html.md) +- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/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) - [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/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) -- [emitEventStep](https://docs.medusajs.com/references/medusa-workflows/steps/emitEventStep/index.html.md) -- [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/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) -- [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md) -- [validatePresenceOfStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePresenceOfStep/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) +- [createCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomersStep/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) - [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md) - [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/index.html.md) - [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md) +- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md) - [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md) -- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/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) -- [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) +- [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/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) -- [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) +- [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md) - [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/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) - [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/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) - [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) +- [deleteFulfillmentSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFulfillmentSetsStep/index.html.md) - [setShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/setShippingOptionsPricesStep/index.html.md) -- [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md) - [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/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) -- [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) +- [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/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) -- [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) -- [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) - [adjustInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/adjustInventoryLevelsStep/index.html.md) - [attachInventoryItemToVariants](https://docs.medusajs.com/references/medusa-workflows/steps/attachInventoryItemToVariants/index.html.md) +- [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/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) - [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) +- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md) - [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md) +- [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/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) -- [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) -- [notifyOnFailureStep](https://docs.medusajs.com/references/medusa-workflows/steps/notifyOnFailureStep/index.html.md) -- [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/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) +- [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) +- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/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) +- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md) +- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md) - [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md) +- [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/index.html.md) - [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md) -- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md) - [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/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) -- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/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) -- [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md) -- [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md) -- [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/index.html.md) -- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md) -- [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/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) -- [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/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) -- [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) -- [declineOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/declineOrderChangeStep/index.html.md) -- [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/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) -- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md) -- [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/index.html.md) -- [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md) -- [previewOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/previewOrderChangeStep/index.html.md) -- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/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) -- [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/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) -- [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md) -- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md) -- [updateOrderShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsStep/index.html.md) -- [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/index.html.md) -- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) - [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/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) +- [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) - [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) - [createPriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListPricesStep/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) +- [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/index.html.md) - [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/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) - [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/index.html.md) - [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md) +- [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md) - [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md) - [createPricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPricePreferencesStep/index.html.md) - [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md) @@ -26463,90 +26421,132 @@ For each product variant, you: - [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/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) +- [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/index.html.md) +- [deleteLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteLineItemsStep/index.html.md) +- [updateLineItemsStepWithSelector](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStepWithSelector/index.html.md) +- [deleteProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductCategoriesStep/index.html.md) +- [createProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductCategoriesStep/index.html.md) +- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/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) +- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md) +- [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/index.html.md) +- [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md) +- [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/index.html.md) +- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md) +- [addOrderTransactionStep](https://docs.medusajs.com/references/medusa-workflows/steps/addOrderTransactionStep/index.html.md) +- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/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) +- [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md) +- [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/index.html.md) +- [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md) +- [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md) +- [updatePromotionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionRulesStep/index.html.md) +- [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/index.html.md) +- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md) +- [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/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) +- [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/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) +- [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) +- [declineOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/declineOrderChangeStep/index.html.md) +- [createOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrdersStep/index.html.md) +- [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/index.html.md) +- [deleteExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteExchangesStep/index.html.md) +- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/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) +- [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) +- [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/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) +- [registerOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderFulfillmentStep/index.html.md) +- [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/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) +- [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) - [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) - [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md) -- [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/index.html.md) +- [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/index.html.md) - [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/index.html.md) -- [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/index.html.md) - [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/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) - [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/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) - [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/index.html.md) +- [deleteCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCollectionsStep/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) +- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md) - [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md) - [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/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) -- [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md) +- [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/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) -- [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) -- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md) -- [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/index.html.md) -- [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md) -- [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/index.html.md) -- [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/index.html.md) -- [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md) -- [updatePromotionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionRulesStep/index.html.md) -- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md) - [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/index.html.md) - [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md) - [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) -- [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) -- [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) - [deleteReturnReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnReasonStep/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) -- [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/index.html.md) -- [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/index.html.md) -- [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md) -- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md) -- [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/index.html.md) -- [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/index.html.md) -- [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/index.html.md) -- [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md) -- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/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) -- [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) +- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/index.html.md) - [updateStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStockLocationsStep/index.html.md) -- [deleteStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStoresStep/index.html.md) +- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md) +- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md) +- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md) +- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md) - [createStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/createStoresStep/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) +- [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) - [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md) - [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md) -- [deleteTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRateRulesStep/index.html.md) -- [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/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) +- [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/index.html.md) - [listTaxRateIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateIdsStep/index.html.md) -- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/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) +- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md) +- [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/index.html.md) +- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/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) +- [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) +- [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md) +- [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/index.html.md) +- [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/index.html.md) +- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/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) +- [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) # Medusa CLI Reference @@ -26692,6 +26692,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.| +# 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 \<host>\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| + + # build Command - Medusa CLI Reference Create a standalone build of the Medusa application. @@ -26754,12 +26770,12 @@ By default, the Medusa Admin is built to the `.medusa/server/public/admin` direc 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 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. +Start the Medusa application in production. ```bash -npx medusa develop +npx medusa start ``` ## Options @@ -26770,6 +26786,35 @@ npx medusa develop |\`-p \<port>\`|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 [<dir_name> [<starter_url>]] +``` + +## 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 \<user>\`|The database user to use for database setup.| +|\`--db-database \<database>\`|The name of the database used for database setup.| +|\`--db-pass \<password>\`|The database password to use for database setup.| +|\`--db-port \<port>\`|The database port to use for database setup.| +|\`--db-host \<host>\`|The database host to use for database setup.| + + # 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. @@ -26831,67 +26876,6 @@ npx medusa plugin:build ``` -# 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 [<dir_name> [<starter_url>]] -``` - -## 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 \<user>\`|The database user to use for database setup.| -|\`--db-database \<database>\`|The name of the database used for database setup.| -|\`--db-pass \<password>\`|The database password to use for database setup.| -|\`--db-port \<port>\`|The database port to use for database setup.| -|\`--db-host \<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| - - -# start Command - Medusa CLI Reference - -Start the Medusa application in production. - -```bash -npx medusa start -``` - -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \<port>\`|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). @@ -26946,6 +26930,22 @@ npx medusa user --email <email> [--password <password>] If ran successfully, you'll receive the invite token in the output.|No|\`false\`| +# 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| + + # Medusa CLI Reference The Medusa CLI tool provides commands that facilitate your development. @@ -26969,6 +26969,129 @@ 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 \<host>\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \<port>\`|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 [<dir_name> [<starter_url>]] +``` + +## 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 \<user>\`|The database user to use for database setup.| +|\`--db-database \<database>\`|The name of the database used for database setup.| +|\`--db-pass \<password>\`|The database password to use for database setup.| +|\`--db-port \<port>\`|The database port to use for database setup.| +|\`--db-host \<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. @@ -27089,97 +27212,6 @@ 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.| -# 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. - - -# 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 [<dir_name> [<starter_url>]] -``` - -## 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 \<user>\`|The database user to use for database setup.| -|\`--db-database \<database>\`|The name of the database used for database setup.| -|\`--db-pass \<password>\`|The database password to use for database setup.| -|\`--db-port \<port>\`|The database port to use for database setup.| -|\`--db-host \<host>\`|The database host to use for database setup.| - - # 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. @@ -27257,55 +27289,23 @@ npx medusa start |\`-p \<port>\`|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| - - -# 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). - -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-cluster -``` - -#### Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-c \<number>\`|The number of CPUs that Medusa can consume.|Medusa will try to consume all CPUs.| -|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| - - -# develop Command - Medusa CLI Reference +# user 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. +Create a new admin user. ```bash -npx medusa develop +npx medusa user --email <email> [--password <password>] ``` ## Options -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`-e \<email>\`|The user's email.|Yes|-| +|\`-p \<password>\`|The user's password.|No|-| +|\`-i \<id>\`|The user's ID.|No|An automatically generated ID.| +|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password. +If ran successfully, you'll receive the invite token in the output.|No|\`false\`| # telemetry Command - Medusa CLI Reference @@ -27324,23 +27324,23 @@ npx medusa telemetry |\`--disable\`|Disable telemetry.| -# user Command - Medusa CLI Reference +# start-cluster Command - Medusa CLI Reference -Create a new admin user. +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 user --email <email> [--password <password>] +npx medusa start-cluster ``` -## Options +#### Options -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`-e \<email>\`|The user's email.|Yes|-| -|\`-p \<password>\`|The user's password.|No|-| -|\`-i \<id>\`|The user's ID.|No|An automatically generated ID.| -|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password. -If ran successfully, you'll receive the invite token in the output.|No|\`false\`| +|Option|Description|Default| +|---|---|---|---|---| +|\`-c \<number>\`|The number of CPUs that Medusa can consume.|Medusa will try to consume all CPUs.| +|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`| # Medusa JS SDK @@ -27644,129 +27644,129 @@ 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) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.list/index.html.md) +- [batchSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.batchSalesChannels/index.html.md) - [revoke](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.revoke/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/index.html.md) - [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/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) - [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/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/Campaign/methods/js_sdk.admin.Campaign.retrieve/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) -- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md) -- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md) -- [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md) -- [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md) -- [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/index.html.md) -- [clearToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken/index.html.md) -- [getTokenStorageInfo\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getTokenStorageInfo_/index.html.md) -- [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/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) -- [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) -- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md) - [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md) -- [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/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) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancel/index.html.md) - [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md) - [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancel/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) - [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md) - [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeInboundItem/index.html.md) - [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/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) - [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/index.html.md) - [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md) +- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/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) - [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md) +- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundShipping/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) - [list](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.list/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) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.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) - [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md) -- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md) +- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md) +- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md) +- [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md) +- [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md) +- [getTokenStorageInfo\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getTokenStorageInfo_/index.html.md) +- [getToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken_/index.html.md) +- [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/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) +- [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/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) +- [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) +- [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/DraftOrder/methods/js_sdk.admin.DraftOrder.create/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) -- [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.list/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/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) - [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.create/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.update/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/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/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) +- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md) - [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/index.html.md) - [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/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) -- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md) +- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md) - [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md) +- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md) - [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/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) - [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/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) -- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md) - [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md) +- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/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) - [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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/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/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md) - [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/index.html.md) - [deleteServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.deleteServiceZone/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) - [updateServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.updateServiceZone/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) +- [batchInventoryItemLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemLocationLevels/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) - [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) -- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md) - [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md) +- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md) - [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/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) - [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/index.html.md) - [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md) -- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/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/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) - [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md) - [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/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) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/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) - [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/index.html.md) - [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md) -- [createFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createFulfillment/index.html.md) - [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/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) @@ -27774,183 +27774,183 @@ The object or class passed to `auth.storage` configuration must have the followi - [markAsDelivered](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.markAsDelivered/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrieve/index.html.md) - [requestTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.requestTransfer/index.html.md) -- [retrievePreview](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrievePreview/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.update/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) - [batchPrices](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.batchPrices/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/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) -- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md) -- [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md) -- [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/index.html.md) -- [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/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) -- [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/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/index.html.md) -- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md) -- [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md) - [batchVariantInventoryItems](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantInventoryItems/index.html.md) - [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md) +- [retrievePreview](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrievePreview/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) +- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.create/index.html.md) - [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md) +- [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md) - [export](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.export/index.html.md) - [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md) +- [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md) - [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/index.html.md) - [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md) - [listVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listVariants/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md) -- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md) - [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md) +- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md) - [updateOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateOption/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/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/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/ProductCategory/methods/js_sdk.admin.ProductCategory.create/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/PricePreference/methods/js_sdk.admin.PricePreference.update/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) -- [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/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) +- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.create/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) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.update/index.html.md) - [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.updateProducts/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md) +- [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.cancelRequest/index.html.md) +- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md) +- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md) +- [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md) +- [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/index.html.md) +- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md) +- [updateOriginalItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateOriginalItem/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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/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/ProductType/methods/js_sdk.admin.ProductType.list/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/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.delete/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.update/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.create/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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md) - [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md) - [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.delete/index.html.md) - [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/index.html.md) -- [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.update/index.html.md) +- [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md) - [updateRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.updateRules/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/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) +- [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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.create/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) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.delete/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.update/index.html.md) -- [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md) -- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/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) +- [update](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.retrieve/index.html.md) +- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.updateProducts/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.create/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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.retrieve/index.html.md) +- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md) - [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md) +- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md) - [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelRequest/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md) +- [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/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) -- [dismissItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.dismissItems/index.html.md) - [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md) -- [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/index.html.md) -- [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/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) +- [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/index.html.md) +- [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/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) - [removeDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeDismissItem/index.html.md) +- [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.retrieve/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) - [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/index.html.md) -- [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/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) -- [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) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/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) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.retrieve/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/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) +- [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.list/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/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) +- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/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) +- [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.retrieve/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) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.retrieve/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/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) - [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md) -- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md) -- [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md) -- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.list/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.list/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/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/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.update/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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.list/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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.update/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) ## JS SDK Auth - [callback](https://docs.medusajs.com/references/js-sdk/auth/callback/index.html.md) -- [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/index.html.md) -- [register](https://docs.medusajs.com/references/js-sdk/auth/register/index.html.md) - [login](https://docs.medusajs.com/references/js-sdk/auth/login/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) +- [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/index.html.md) - [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md) +- [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md) ## JS SDK Store @@ -27958,10 +27958,10 @@ The object or class passed to `auth.storage` configuration must have the followi - [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) -- [customer](https://docs.medusajs.com/references/js-sdk/store/customer/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) +- [customer](https://docs.medusajs.com/references/js-sdk/store/customer/index.html.md) - [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md) -- [order](https://docs.medusajs.com/references/js-sdk/store/order/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) @@ -28770,6 +28770,65 @@ 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. +# Container - Admin Components + +The Medusa Admin wraps each section of a page in a container. + + + +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 +import { + Container as UiContainer, + clx, +} from "@medusajs/ui" + +type ContainerProps = React.ComponentProps<typeof UiContainer> + +export const Container = (props: ContainerProps) => { + return ( + <UiContainer {...props} className={clx( + "divide-y p-0", + props.className + )} /> + ) +} +``` + +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. + +*** + +## 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: + +```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 ( + <Container> + <Header title="Product Widget" /> + </Container> + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +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. + + # Action Menu - Admin Components The Medusa Admin often provides additional actions in a dropdown shown when users click a three-dot icon. @@ -29431,129 +29490,505 @@ export const EditForm = () => { console.log(name) }) - // TODO render form + // TODO render form +} +``` + +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. + +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. + +### Render Components + +You'll now add a `return` statement that renders the drawer where the form is shown. + +Replace `// TODO render form` with the following: + +```tsx title="src/admin/components/edit-form.tsx" +// other imports... +import { + Drawer, + Heading, + Label, + Input, + Button, +} from "@medusajs/ui" +import { + FormProvider, + Controller, +} from "react-hook-form" + +export const EditForm = () => { + // ... + + return ( + <Drawer> + <Drawer.Trigger asChild> + <Button>Edit Item</Button> + </Drawer.Trigger> + <Drawer.Content> + <FormProvider {...form}> + <form + onSubmit={handleSubmit} + className="flex flex-1 flex-col overflow-hidden" + > + <Drawer.Header> + <Heading className="capitalize"> + Edit Item + </Heading> + </Drawer.Header> + <Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto"> + <Controller + control={form.control} + name="name" + render={({ field }) => { + return ( + <div className="flex flex-col space-y-2"> + <div className="flex items-center gap-x-1"> + <Label size="small" weight="plus"> + Name + </Label> + </div> + <Input {...field} /> + </div> + ) + }} + /> + </Drawer.Body> + <Drawer.Footer> + <div className="flex items-center justify-end gap-x-2"> + <Drawer.Close asChild> + <Button size="small" variant="secondary"> + Cancel + </Button> + </Drawer.Close> + <Button size="small" type="submit"> + Save + </Button> + </div> + </Drawer.Footer> + </form> + </FormProvider> + </Drawer.Content> + </Drawer> + ) +} +``` + +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`. + +Finally, in the `Drawer.Footer` component, you add buttons to save or cancel the form submission. + +### Use Edit Form Component + +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 { Container } from "../components/container" +import { Header } from "../components/header" +import { EditForm } from "../components/edit-form" + +const ProductWidget = () => { + return ( + <Container> + <Header + title="Items" + actions={[ + { + type: "custom", + children: <EditForm />, + }, + ]} + /> + </Container> + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +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. + + + +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" + +export type HeadingProps = { + title: string + subtitle?: string + actions?: ( + { + type: "button", + props: React.ComponentProps<typeof Button> + link?: LinkProps + } | + { + type: "action-menu" + props: ActionMenuProps + } | + { + type: "custom" + children: React.ReactNode + } + )[] +} + +export const Header = ({ + title, + subtitle, + actions = [], +}: HeadingProps) => { + return ( + <div className="flex items-center justify-between px-6 py-4"> + <div> + <Heading level="h2">{title}</Heading> + {subtitle && ( + <Text className="text-ui-fg-subtle" size="small"> + {subtitle} + </Text> + )} + </div> + {actions.length > 0 && ( + <div className="flex items-center justify-center gap-x-2"> + {actions.map((action, index) => ( + <> + {action.type === "button" && ( + <Button + {...action.props} + size={action.props.size || "small"} + key={index} + > + <> + {action.props.children} + {action.link && <Link {...action.link} />} + </> + </Button> + )} + {action.type === "action-menu" && ( + <ActionMenu {...action.props} /> + )} + {action.type === "custom" && action.children} + </> + ))} + </div> + )} + </div> + ) +} +``` + +The `Header` component shows a title, and optionally a subtitle and action buttons. + +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. + +It accepts the following props: + +- title: (\`string\`) The section's title. +- subtitle: (\`string\`) The section's subtitle. +- actions: (\`object\[]\`) An array of actions to show. + + - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add. + + \- If its value is \`button\`, it'll show a button that can have a link or an on-click action. + + \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions. + + \- If its value is \`custom\`, you can pass any React nodes to render. + + - props: (object) + + - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions. + +*** + +## Example + +Use the `Header` 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 { Container } from "../components/container" +import { Header } from "../components/header" + +const ProductWidget = () => { + return ( + <Container> + <Header + title="Product Widget" + subtitle="This is my custom product widget" + actions={[ + { + type: "button", + props: { + children: "Click me", + variant: "secondary", + onClick: () => { + alert("You clicked the button.") + }, + }, + }, + ]} + /> + </Container> + ) } + +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 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. +# JSON View - Admin Components -### Render Components +Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format. -You'll now add a `return` statement that renders the drawer where the form is shown. + -Replace `// TODO render form` with the following: +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" -// other imports... -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 { - FormProvider, - Controller, -} from "react-hook-form" +import Primitive from "@uiw/react-json-view" +import { CSSProperties, MouseEvent, Suspense, useState } from "react" -export const EditForm = () => { - // ... +type JsonViewSectionProps = { + data: object + title?: string +} + +export const JsonViewSection = ({ data }: JsonViewSectionProps) => { + const numberOfKeys = Object.keys(data).length return ( - <Drawer> - <Drawer.Trigger asChild> - <Button>Edit Item</Button> - </Drawer.Trigger> - <Drawer.Content> - <FormProvider {...form}> - <form - onSubmit={handleSubmit} - className="flex flex-1 flex-col overflow-hidden" + <Container className="flex items-center justify-between px-6 py-4"> + <div className="flex items-center gap-x-4"> + <Heading level="h2">JSON</Heading> + <Badge size="2xsmall" rounded="full"> + {numberOfKeys} keys + </Badge> + </div> + <Drawer> + <Drawer.Trigger asChild> + <IconButton + size="small" + variant="transparent" + className="text-ui-fg-muted hover:text-ui-fg-subtle" > - <Drawer.Header> - <Heading className="capitalize"> - Edit Item - </Heading> - </Drawer.Header> - <Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto"> - <Controller - control={form.control} - name="name" - render={({ field }) => { - return ( - <div className="flex flex-col space-y-2"> - <div className="flex items-center gap-x-1"> - <Label size="small" weight="plus"> - Name - </Label> - </div> - <Input {...field} /> - </div> - ) - }} - /> - </Drawer.Body> - <Drawer.Footer> - <div className="flex items-center justify-end gap-x-2"> + <ArrowUpRightOnBox /> + </IconButton> + </Drawer.Trigger> + <Drawer.Content className="bg-ui-contrast-bg-base text-ui-code-fg-subtle !shadow-elevation-commandbar overflow-hidden border border-none max-md:inset-x-2 max-md:max-w-[calc(100%-16px)]"> + <div className="bg-ui-code-bg-base flex items-center justify-between px-6 py-4"> + <div className="flex items-center gap-x-4"> + <Drawer.Title asChild> + <Heading className="text-ui-contrast-fg-primary"> + <span className="text-ui-fg-subtle"> + {numberOfKeys} + </span> + </Heading> + </Drawer.Title> + </div> + <div className="flex items-center gap-x-2"> + <Kbd className="bg-ui-contrast-bg-subtle border-ui-contrast-border-base text-ui-contrast-fg-secondary"> + esc + </Kbd> <Drawer.Close asChild> - <Button size="small" variant="secondary"> - Cancel - </Button> + <IconButton + size="small" + variant="transparent" + className="text-ui-contrast-fg-secondary hover:text-ui-contrast-fg-primary hover:bg-ui-contrast-bg-base-hover active:bg-ui-contrast-bg-base-pressed focus-visible:bg-ui-contrast-bg-base-hover focus-visible:shadow-borders-interactive-with-active" + > + <XMarkMini /> + </IconButton> </Drawer.Close> - <Button size="small" type="submit"> - Save - </Button> </div> - </Drawer.Footer> - </form> - </FormProvider> - </Drawer.Content> - </Drawer> + </div> + <Drawer.Body className="flex flex-1 flex-col overflow-hidden px-[5px] py-0 pb-[5px]"> + <div className="bg-ui-contrast-bg-subtle flex-1 overflow-auto rounded-b-[4px] rounded-t-lg p-3"> + <Suspense + fallback={<div className="flex size-full flex-col"></div>} + > + <Primitive + value={data} + displayDataTypes={false} + style={ + { + "--w-rjv-font-family": "Roboto Mono, monospace", + "--w-rjv-line-color": "var(--contrast-border-base)", + "--w-rjv-curlybraces-color": + "var(--contrast-fg-secondary)", + "--w-rjv-brackets-color": "var(--contrast-fg-secondary)", + "--w-rjv-key-string": "var(--contrast-fg-primary)", + "--w-rjv-info-color": "var(--contrast-fg-secondary)", + "--w-rjv-type-string-color": "var(--tag-green-icon)", + "--w-rjv-quotes-string-color": "var(--tag-green-icon)", + "--w-rjv-type-boolean-color": "var(--tag-orange-icon)", + "--w-rjv-type-int-color": "var(--tag-orange-icon)", + "--w-rjv-type-float-color": "var(--tag-orange-icon)", + "--w-rjv-type-bigint-color": "var(--tag-orange-icon)", + "--w-rjv-key-number": "var(--contrast-fg-secondary)", + "--w-rjv-arrow-color": "var(--contrast-fg-secondary)", + "--w-rjv-copied-color": "var(--contrast-fg-secondary)", + "--w-rjv-copied-success-color": + "var(--contrast-fg-primary)", + "--w-rjv-colon-color": "var(--contrast-fg-primary)", + "--w-rjv-ellipsis-color": "var(--contrast-fg-secondary)", + } as CSSProperties + } + collapsed={1} + > + <Primitive.Quote render={() => <span />} /> + <Primitive.Null + render={() => ( + <span className="text-ui-tag-red-icon">null</span> + )} + /> + <Primitive.Undefined + render={() => ( + <span className="text-ui-tag-blue-icon">undefined</span> + )} + /> + <Primitive.CountInfo + render={(_props, { value }) => { + return ( + <span className="text-ui-contrast-fg-secondary ml-2"> + {Object.keys(value as object).length} items + </span> + ) + }} + /> + <Primitive.Arrow> + <TriangleDownMini className="text-ui-contrast-fg-secondary -ml-[0.5px]" /> + </Primitive.Arrow> + <Primitive.Colon> + <span className="mr-1">:</span> + </Primitive.Colon> + <Primitive.Copied + render={({ style }, { value }) => { + return <Copied style={style} value={value} /> + }} + /> + </Primitive> + </Suspense> + </div> + </Drawer.Body> + </Drawer.Content> + </Drawer> + </Container> ) } -``` -You render a drawer, with a trigger button to open it. +type CopiedProps = { + style?: CSSProperties + value: object | undefined +} -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. +const Copied = ({ style, value }: CopiedProps) => { + const [copied, setCopied] = useState(false) -In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event. + const handler = (e: MouseEvent<HTMLSpanElement>) => { + e.stopPropagation() + setCopied(true) -You render the form's components inside the `Drawer.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`. + if (typeof value === "string") { + navigator.clipboard.writeText(value) + } else { + const json = JSON.stringify(value, null, 2) + navigator.clipboard.writeText(json) + } -Finally, in the `Drawer.Footer` component, you add buttons to save or cancel the form submission. + setTimeout(() => { + setCopied(false) + }, 2000) + } -### Use Edit Form Component + const styl = { whiteSpace: "nowrap", width: "20px" } -You can use the `EditForm` component in your widget or UI route. + if (copied) { + return ( + <span style={{ ...style, ...styl }}> + <Check className="text-ui-contrast-fg-primary" /> + </span> + ) + } + + return ( + <span style={{ ...style, ...styl }} onClick={handler}> + <SquareTwoStack className="text-ui-contrast-fg-secondary" /> + </span> + ) +} +``` + +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. + +The `JsonViewSection` accepts a `data` prop, which is the data to show as a JSON object in the drawer. + +*** + +## 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 { Container } from "../components/container" -import { Header } from "../components/header" -import { EditForm } from "../components/edit-form" +import { JsonViewSection } from "../components/json-view-section" const ProductWidget = () => { - return ( - <Container> - <Header - title="Items" - actions={[ - { - type: "custom", - children: <EditForm />, - }, - ]} - /> - </Container> - ) + return <JsonViewSection data={{ + name: "John", + }} /> } export const config = defineWidgetConfig({ @@ -29563,9 +29998,7 @@ 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. +This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`. # Data Table - Admin Components @@ -30055,211 +30488,6 @@ export default CustomPage ``` -# 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. - - - -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" - -export type HeadingProps = { - title: string - subtitle?: string - actions?: ( - { - type: "button", - props: React.ComponentProps<typeof Button> - link?: LinkProps - } | - { - type: "action-menu" - props: ActionMenuProps - } | - { - type: "custom" - children: React.ReactNode - } - )[] -} - -export const Header = ({ - title, - subtitle, - actions = [], -}: HeadingProps) => { - return ( - <div className="flex items-center justify-between px-6 py-4"> - <div> - <Heading level="h2">{title}</Heading> - {subtitle && ( - <Text className="text-ui-fg-subtle" size="small"> - {subtitle} - </Text> - )} - </div> - {actions.length > 0 && ( - <div className="flex items-center justify-center gap-x-2"> - {actions.map((action, index) => ( - <> - {action.type === "button" && ( - <Button - {...action.props} - size={action.props.size || "small"} - key={index} - > - <> - {action.props.children} - {action.link && <Link {...action.link} />} - </> - </Button> - )} - {action.type === "action-menu" && ( - <ActionMenu {...action.props} /> - )} - {action.type === "custom" && action.children} - </> - ))} - </div> - )} - </div> - ) -} -``` - -The `Header` component shows a title, and optionally a subtitle and action buttons. - -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. - -It accepts the following props: - -- title: (\`string\`) The section's title. -- subtitle: (\`string\`) The section's subtitle. -- actions: (\`object\[]\`) An array of actions to show. - - - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add. - - \- If its value is \`button\`, it'll show a button that can have a link or an on-click action. - - \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions. - - \- If its value is \`custom\`, you can pass any React nodes to render. - - - props: (object) - - - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions. - -*** - -## Example - -Use the `Header` 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 { Container } from "../components/container" -import { Header } from "../components/header" - -const ProductWidget = () => { - return ( - <Container> - <Header - title="Product Widget" - subtitle="This is my custom product widget" - actions={[ - { - type: "button", - props: { - children: "Click me", - variant: "secondary", - onClick: () => { - alert("You clicked the button.") - }, - }, - }, - ]} - /> - </Container> - ) -} - -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. - - -# Container - Admin Components - -The Medusa Admin wraps each section of a page in a container. - - - -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 -import { - Container as UiContainer, - clx, -} from "@medusajs/ui" - -type ContainerProps = React.ComponentProps<typeof UiContainer> - -export const Container = (props: ContainerProps) => { - return ( - <UiContainer {...props} className={clx( - "divide-y p-0", - props.className - )} /> - ) -} -``` - -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. - -*** - -## 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: - -```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 ( - <Container> - <Header title="Product Widget" /> - </Container> - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -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. - - # Section Row - Admin Components The Medusa Admin often shows information in rows of label-values, such as when showing a product's details. @@ -30642,234 +30870,6 @@ If `data` isn't `undefined`, you display the `Table` component passing it the fo To test it out, log into the Medusa Admin and open `http://localhost:9000/app/custom`. You'll find a table of products with pagination. -# JSON View - Admin Components - -Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format. - - - -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/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" - -type JsonViewSectionProps = { - data: object - title?: string -} - -export const JsonViewSection = ({ data }: JsonViewSectionProps) => { - const numberOfKeys = Object.keys(data).length - - return ( - <Container className="flex items-center justify-between px-6 py-4"> - <div className="flex items-center gap-x-4"> - <Heading level="h2">JSON</Heading> - <Badge size="2xsmall" rounded="full"> - {numberOfKeys} keys - </Badge> - </div> - <Drawer> - <Drawer.Trigger asChild> - <IconButton - size="small" - variant="transparent" - className="text-ui-fg-muted hover:text-ui-fg-subtle" - > - <ArrowUpRightOnBox /> - </IconButton> - </Drawer.Trigger> - <Drawer.Content className="bg-ui-contrast-bg-base text-ui-code-fg-subtle !shadow-elevation-commandbar overflow-hidden border border-none max-md:inset-x-2 max-md:max-w-[calc(100%-16px)]"> - <div className="bg-ui-code-bg-base flex items-center justify-between px-6 py-4"> - <div className="flex items-center gap-x-4"> - <Drawer.Title asChild> - <Heading className="text-ui-contrast-fg-primary"> - <span className="text-ui-fg-subtle"> - {numberOfKeys} - </span> - </Heading> - </Drawer.Title> - </div> - <div className="flex items-center gap-x-2"> - <Kbd className="bg-ui-contrast-bg-subtle border-ui-contrast-border-base text-ui-contrast-fg-secondary"> - esc - </Kbd> - <Drawer.Close asChild> - <IconButton - size="small" - variant="transparent" - className="text-ui-contrast-fg-secondary hover:text-ui-contrast-fg-primary hover:bg-ui-contrast-bg-base-hover active:bg-ui-contrast-bg-base-pressed focus-visible:bg-ui-contrast-bg-base-hover focus-visible:shadow-borders-interactive-with-active" - > - <XMarkMini /> - </IconButton> - </Drawer.Close> - </div> - </div> - <Drawer.Body className="flex flex-1 flex-col overflow-hidden px-[5px] py-0 pb-[5px]"> - <div className="bg-ui-contrast-bg-subtle flex-1 overflow-auto rounded-b-[4px] rounded-t-lg p-3"> - <Suspense - fallback={<div className="flex size-full flex-col"></div>} - > - <Primitive - value={data} - displayDataTypes={false} - style={ - { - "--w-rjv-font-family": "Roboto Mono, monospace", - "--w-rjv-line-color": "var(--contrast-border-base)", - "--w-rjv-curlybraces-color": - "var(--contrast-fg-secondary)", - "--w-rjv-brackets-color": "var(--contrast-fg-secondary)", - "--w-rjv-key-string": "var(--contrast-fg-primary)", - "--w-rjv-info-color": "var(--contrast-fg-secondary)", - "--w-rjv-type-string-color": "var(--tag-green-icon)", - "--w-rjv-quotes-string-color": "var(--tag-green-icon)", - "--w-rjv-type-boolean-color": "var(--tag-orange-icon)", - "--w-rjv-type-int-color": "var(--tag-orange-icon)", - "--w-rjv-type-float-color": "var(--tag-orange-icon)", - "--w-rjv-type-bigint-color": "var(--tag-orange-icon)", - "--w-rjv-key-number": "var(--contrast-fg-secondary)", - "--w-rjv-arrow-color": "var(--contrast-fg-secondary)", - "--w-rjv-copied-color": "var(--contrast-fg-secondary)", - "--w-rjv-copied-success-color": - "var(--contrast-fg-primary)", - "--w-rjv-colon-color": "var(--contrast-fg-primary)", - "--w-rjv-ellipsis-color": "var(--contrast-fg-secondary)", - } as CSSProperties - } - collapsed={1} - > - <Primitive.Quote render={() => <span />} /> - <Primitive.Null - render={() => ( - <span className="text-ui-tag-red-icon">null</span> - )} - /> - <Primitive.Undefined - render={() => ( - <span className="text-ui-tag-blue-icon">undefined</span> - )} - /> - <Primitive.CountInfo - render={(_props, { value }) => { - return ( - <span className="text-ui-contrast-fg-secondary ml-2"> - {Object.keys(value as object).length} items - </span> - ) - }} - /> - <Primitive.Arrow> - <TriangleDownMini className="text-ui-contrast-fg-secondary -ml-[0.5px]" /> - </Primitive.Arrow> - <Primitive.Colon> - <span className="mr-1">:</span> - </Primitive.Colon> - <Primitive.Copied - render={({ style }, { value }) => { - return <Copied style={style} value={value} /> - }} - /> - </Primitive> - </Suspense> - </div> - </Drawer.Body> - </Drawer.Content> - </Drawer> - </Container> - ) -} - -type CopiedProps = { - style?: CSSProperties - value: object | undefined -} - -const Copied = ({ style, value }: CopiedProps) => { - const [copied, setCopied] = useState(false) - - const handler = (e: MouseEvent<HTMLSpanElement>) => { - e.stopPropagation() - setCopied(true) - - if (typeof value === "string") { - navigator.clipboard.writeText(value) - } else { - const json = JSON.stringify(value, null, 2) - navigator.clipboard.writeText(json) - } - - setTimeout(() => { - setCopied(false) - }, 2000) - } - - const styl = { whiteSpace: "nowrap", width: "20px" } - - if (copied) { - return ( - <span style={{ ...style, ...styl }}> - <Check className="text-ui-contrast-fg-primary" /> - </span> - ) - } - - return ( - <span style={{ ...style, ...styl }} onClick={handler}> - <SquareTwoStack className="text-ui-contrast-fg-secondary" /> - </span> - ) -} -``` - -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. - -The `JsonViewSection` accepts a `data` prop, which is the data to show as a JSON object in the drawer. - -*** - -## 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 <JsonViewSection data={{ - name: "John", - }} /> -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`. - - # Service Factory Reference This section of the documentation provides a reference of the methods generated for services extending the service factory (`MedusaService`), and how to use them. @@ -31196,46 +31196,6 @@ To sort records by one or more properties, pass to the second object parameter t The method returns an array of the first `15` records matching the filters. -# delete Method - Service Factory Reference - -This method deletes one or more records. - -## Delete One Record - -```ts -await postModuleService.deletePosts("123") -``` - -To delete one record, pass its ID as a parameter of the method. - -*** - -## Delete Multiple Records - -```ts -await postModuleService.deletePosts([ - "123", - "321", -]) -``` - -To delete multiple records, pass an array of IDs as a parameter of the method. - -*** - -## Delete Records Matching Filters - -```ts -await postModuleService.deletePosts({ - name: "My Post", -}) -``` - -To 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). - - # listAndCount Method - Service Factory Reference This method retrieves a list of records with the total count. @@ -31459,6 +31419,63 @@ restoredPosts = { ``` +# retrieve Method - Service Factory Reference + +This method retrieves one record of the data model by its ID. + +## Retrieve a Record + +```ts +const post = await postModuleService.retrievePost("123") +``` + +### Parameters + +Pass the ID of the record to retrieve. + +### Returns + +The method returns the record as an object. + +*** + +## Retrieve a Record's 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 post = await postModuleService.retrievePost("123", { + relations: ["author"], +}) +``` + +### Parameters + +To retrieve the data model with relations, pass as a second parameter of the method an object with the property `relations`. `relations`'s value is an array of relation names. + +### Returns + +The method returns the record as an object. + +*** + +## Select Properties to Retrieve + +```ts +const post = await postModuleService.retrievePost("123", { + select: ["id", "name"], +}) +``` + +### Parameters + +By default, all of the record's properties are retrieved. To select specific ones, pass in the second object parameter a `select` property. Its value is an array of property names. + +### Returns + +The method returns the record as an object. + + # softDelete Method - Service Factory Reference This method soft deletes one or more records of the data model. @@ -31546,61 +31563,44 @@ deletedPosts = { ``` -# retrieve Method - Service Factory Reference +# delete Method - Service Factory Reference -This method retrieves one record of the data model by its ID. +This method deletes one or more records. -## Retrieve a Record +## Delete One Record ```ts -const post = await postModuleService.retrievePost("123") +await postModuleService.deletePosts("123") ``` -### Parameters - -Pass the ID of the record to retrieve. - -### Returns - -The method returns the record as an object. +To delete one record, pass its ID as a parameter of the method. *** -## Retrieve a Record's 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). +## Delete Multiple Records ```ts -const post = await postModuleService.retrievePost("123", { - relations: ["author"], -}) +await postModuleService.deletePosts([ + "123", + "321", +]) ``` -### Parameters - -To retrieve the data model with relations, pass as a second parameter of the method an object with the property `relations`. `relations`'s value is an array of relation names. - -### Returns - -The method returns the record as an object. +To delete multiple records, pass an array of IDs as a parameter of the method. *** -## Select Properties to Retrieve +## Delete Records Matching Filters ```ts -const post = await postModuleService.retrievePost("123", { - select: ["id", "name"], +await postModuleService.deletePosts({ + name: "My Post", }) ``` -### Parameters - -By default, all of the record's properties are retrieved. To select specific ones, pass in the second object parameter a `select` property. Its value is an array of property names. - -### Returns +To delete records matching a set of filters, pass an object of filters as a parameter. -The method returns the record as an object. +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). # update Method - Service Factory Reference @@ -32167,6 +32167,125 @@ How to install and setup Medusa UI. +# Medusa Admin Extension + +How to install and use Medusa UI for building Admin extensions. + +## Installation + +*** + +The `@medusajs/ui` package is a already installed as a dependency of the `@medusajs/admin` package. Due to this you can simply import the package and use it in your local Admin extensions. + +If you are building a Admin extension as part of a Medusa plugin, you can install the package as a dependency of your plugin. + +```bash +npm install @medusajs/ui +``` + +## Configuration + +*** + +The configuration of the UI package is handled by the `@medusajs/admin` package. Therefore, you do not need to any additional configuration to use the UI package in your Admin extensions. + + +# Standalone Project + +How to install and use Medusa UI in a standalone project. + +## Installation + +*** + +Medusa UI is a React UI library and while it's intended for usage within Medusa projects, it can also be used in any React project. + +### Install Medusa UI + +Install the React UI library with the following command: + +```bash +npm install @medusajs/ui +``` + +### Configuring Tailwind CSS + +The components are styled using Tailwind CSS, and in order to use them, you will need to install Tailwind CSS in your project as well. +For more information on how to install Tailwind CSS, please refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs/installation). + +All of the classes used for Medusa UI are shipped as a Tailwind CSS customization. +You can install it with the following command: + +```bash +npm install @medusajs/ui-preset +``` + +After you have installed Tailwind CSS and the Medusa UI preset, you need to add the following to your `tailwind.config.js`file: + +```tsx +module.exports = { + presets: [require("@medusajs/ui-preset")], + // ... +} +``` + +In order for the styles to be applied correctly to the components, you will also need to ensure that +`@medusajs/ui` is included in the content field of your `tailwind.config.js` file: + +```tsx +module.exports = { + content: [ + // ... + "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}", + ], + // ... +} +``` + +If you are working within a monorepo, you may need to add the path to the `@medusajs/ui` package in your `tailwind.config.js` like so: + +```tsx +const path = require("path") + +const uiPath = path.resolve( + require.resolve("@medusajs/ui"), + "../..", + "\*_/_.{js,jsx,ts,tsx}" +) + +module.exports = { + content: [ + // ... + uiPath, + ], + // ... +} + +``` + +## Start building + +*** + +You are now ready to start building your application with Medusa UI. You can import the components like so: + +```tsx +import { Button, Drawer } from "@medusajs/ui" +``` + +## Updating UI Packages + +*** + +Medusa's design-system packages, including `@medusajs/ui`, `@medusajs/ui-preset`, and `@medusajs/ui-icons`, are versioned independently. However, they're still part of the latest Medusa release. So, you can browse the [release notes](https://github.com/medusajs/medusa/releases) to see if there are any breaking changes to these packages. + +To update these packages, update their version in your `package.json` file and re-install dependencies. For example: + +```bash +npm install @medusajs/ui +``` + + # Alert A component for displaying important messages. @@ -38562,125 +38681,6 @@ If you're using the `Tooltip` component in a project other than the Medusa Admin - disableHoverableContent: (boolean) When \`true\`, trying to hover the content will result in the tooltip closing as the pointer leaves the trigger. -# Medusa Admin Extension - -How to install and use Medusa UI for building Admin extensions. - -## Installation - -*** - -The `@medusajs/ui` package is a already installed as a dependency of the `@medusajs/admin` package. Due to this you can simply import the package and use it in your local Admin extensions. - -If you are building a Admin extension as part of a Medusa plugin, you can install the package as a dependency of your plugin. - -```bash -npm install @medusajs/ui -``` - -## Configuration - -*** - -The configuration of the UI package is handled by the `@medusajs/admin` package. Therefore, you do not need to any additional configuration to use the UI package in your Admin extensions. - - -# Standalone Project - -How to install and use Medusa UI in a standalone project. - -## Installation - -*** - -Medusa UI is a React UI library and while it's intended for usage within Medusa projects, it can also be used in any React project. - -### Install Medusa UI - -Install the React UI library with the following command: - -```bash -npm install @medusajs/ui -``` - -### Configuring Tailwind CSS - -The components are styled using Tailwind CSS, and in order to use them, you will need to install Tailwind CSS in your project as well. -For more information on how to install Tailwind CSS, please refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs/installation). - -All of the classes used for Medusa UI are shipped as a Tailwind CSS customization. -You can install it with the following command: - -```bash -npm install @medusajs/ui-preset -``` - -After you have installed Tailwind CSS and the Medusa UI preset, you need to add the following to your `tailwind.config.js`file: - -```tsx -module.exports = { - presets: [require("@medusajs/ui-preset")], - // ... -} -``` - -In order for the styles to be applied correctly to the components, you will also need to ensure that -`@medusajs/ui` is included in the content field of your `tailwind.config.js` file: - -```tsx -module.exports = { - content: [ - // ... - "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}", - ], - // ... -} -``` - -If you are working within a monorepo, you may need to add the path to the `@medusajs/ui` package in your `tailwind.config.js` like so: - -```tsx -const path = require("path") - -const uiPath = path.resolve( - require.resolve("@medusajs/ui"), - "../..", - "\*_/_.{js,jsx,ts,tsx}" -) - -module.exports = { - content: [ - // ... - uiPath, - ], - // ... -} - -``` - -## Start building - -*** - -You are now ready to start building your application with Medusa UI. You can import the components like so: - -```tsx -import { Button, Drawer } from "@medusajs/ui" -``` - -## Updating UI Packages - -*** - -Medusa's design-system packages, including `@medusajs/ui`, `@medusajs/ui-preset`, and `@medusajs/ui-icons`, are versioned independently. However, they're still part of the latest Medusa release. So, you can browse the [release notes](https://github.com/medusajs/medusa/releases) to see if there are any breaking changes to these packages. - -To update these packages, update their version in your `package.json` file and re-install dependencies. For example: - -```bash -npm install @medusajs/ui -``` - - # clx Utility function for working with classNames. From a9e1c4a8c84dd7cb7761e085c225066cd956d203 Mon Sep 17 00:00:00 2001 From: Shahed Nasser <shahednasser@gmail.com> Date: Tue, 4 Mar 2025 12:21:42 +0200 Subject: [PATCH 4/4] small fix --- .../customers/reset-password/page.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/www/apps/resources/app/storefront-development/customers/reset-password/page.mdx b/www/apps/resources/app/storefront-development/customers/reset-password/page.mdx index bb321e3f05595..8c51bbee29972 100644 --- a/www/apps/resources/app/storefront-development/customers/reset-password/page.mdx +++ b/www/apps/resources/app/storefront-development/customers/reset-password/page.mdx @@ -167,8 +167,8 @@ export const resetPasswordFetchHighlights = [ ["3", "email", "Receive the email from a query parameter."], ["9", "password", "Assuming the password is retrieved from an input field."], ["14", "fetch", "Send a request to update the customer's password."], - ["14", "token", "Pass the token as a query parameter."], - ["20", "body", "Pass the email and password in the request body."] + ["19", "token", "Pass the token in the Authorization header."], + ["21", "body", "Pass the email and password in the request body."] ] ```ts highlights={resetPasswordFetchHighlights} @@ -211,8 +211,8 @@ export const resetPasswordHighlights = [ ["18", "token", "Receive the token from a query parameter."], ["21", "email", "Receive the email from a query parameter."], ["35", "fetch", "Send a request to update the customer's password."], - ["35", "token", "Pass the token as a query parameter."], - ["41", "body", "Pass the email and password in the request body."] + ["40", "token", "Pass the token in the Authorization header."], + ["42", "body", "Pass the email and password in the request body."] ] ```tsx highlights={resetPasswordHighlights}