diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index 271d69788e..61c10fad09 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -136,6 +136,76 @@ 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. +# 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 + +![Diagram showcasing the connection between the three deployed components](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600807/Medusa%20Book/deployment-options_ceuuvo.jpg) + +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… + +![Diagram showcasing how the Medusa server and its associated services would be deployed](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600972/Medusa%20Book/backend_deployment_pgexo3.jpg) + +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. + +### Deploy Server and Worker Instances + +By default, Medusa runs all processes in a single instance. This includes the server that handles incoming requests and the worker that processes background tasks. While this works for development, it’s not optimal for production environments as many background tasks can be long-running or resource-heavy. + +Instead, you should deploy two instances: + +- A server instance, which handles incoming requests to the application’s API routes. +- A worker instance, which processes background tasks, including scheduled jobs and subscribers. + +You don’t need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables. + +Learn more about worker modes and how to configure them in the [Worker Mode chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md). + +### 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/pricing) + +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. @@ -260,76 +330,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 - -![Diagram showcasing the connection between the three deployed components](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600807/Medusa%20Book/deployment-options_ceuuvo.jpg) - -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… - -![Diagram showcasing how the Medusa server and its associated services would be deployed](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600972/Medusa%20Book/backend_deployment_pgexo3.jpg) - -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. - -### Deploy Server and Worker Instances - -By default, Medusa runs all processes in a single instance. This includes the server that handles incoming requests and the worker that processes background tasks. While this works for development, it’s not optimal for production environments as many background tasks can be long-running or resource-heavy. - -Instead, you should deploy two instances: - -- A server instance, which handles incoming requests to the application’s API routes. -- A worker instance, which processes background tasks, including scheduled jobs and subscribers. - -You don’t need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables. - -Learn more about worker modes and how to configure them in the [Worker Mode chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md). - -### 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/pricing) - -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. - - # 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. @@ -1405,6 +1405,305 @@ import { BrandModuleService } from "@/modules/brand/service" ``` +# 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/pricing) + +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 worker mode in the [Worker Module chapter](https://docs.medusajs.com/learn/production/worker-mode/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 + +The `redisUrl` configuration specifies the connection URL to Redis to store the Medusa server's session. + +Learn more in the [Medusa Configuration documentation](https://docs.medusajs.com/learn/configurations/medusa-config#redisurl/index.html.md). + +So, add the following configuration in `medusa-config.ts` : + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + // ... + redisUrl: process.env.REDIS_URL, + }, +}) +``` + +*** + +## 2. Add predeploy Script + +Before you start the Medusa application in production, you should always run migrations and sync links. + +So, add the following script in `package.json`: + +```json +"scripts": { + // ... + "predeploy": "medusa db:migrate" +}, +``` + +*** + +## 3. Install Production Modules and Providers + +By default, your Medusa application uses modules and providers useful for development, such as the In-Memory Cache Module or the Local File Module Provider. + +It’s highly recommended to instead use modules and providers suitable for production, including: + +- [Redis Cache Module](https://docs.medusajs.com/resources/architectural-modules/cache/redis/index.html.md) +- [Redis Event Bus Module](https://docs.medusajs.com/resources/architectural-modules/event/redis/index.html.md) +- [Workflow Engine Redis Module](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/redis/index.html.md) +- [S3 File Module Provider](https://docs.medusajs.com/resources/architectural-modules/file/s3/index.html.md) (or other file module providers production-ready). +- [SendGrid Notification Module Provider](https://docs.medusajs.com/resources/architectural-modules/notification/sendgrid/index.html.md) (or other notification module providers production-ready). + +Then, add these modules in `medusa-config.ts`: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/cache-redis", + options: { + redisUrl: process.env.REDIS_URL, + }, + }, + { + resolve: "@medusajs/medusa/event-bus-redis", + options: { + redisUrl: process.env.REDIS_URL, + }, + }, + { + resolve: "@medusajs/medusa/workflow-engine-redis", + options: { + redis: { + url: process.env.REDIS_URL, + }, + }, + }, + ], +}) +``` + +Check out the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) and [Architectural Modules](https://docs.medusajs.com/resources/architectural-modules/index.html.md) documentation for other modules and providers to use. + +*** + +## 4. Create PostgreSQL and Redis Databases + +Your Medusa application must connect to PostgreSQL and Redis databases. So, before you deploy it, create production PostgreSQL and Redis databases. + +If your hosting provider doesn't support databases, you can use [Neon for PostgreSQL database hosting](https://neon.tech/), and [Redis Cloud for the Redis database hosting](https://redis.io/cloud/). + +After hosting both databases, keep their connection URLs for the next steps. + +*** + +## 5. Deploy Medusa Application in Server Mode + +As mentioned earlier, you'll deploy two instances or create two deployments of your Medusa application: one in server mode, and the other in worker mode. + +The deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. + +### Set Environment Variables + +When setting the environment variables of the Medusa application, set the following variables: + +```bash +COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET +JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET +STORE_CORS= # STOREFRONT URL +ADMIN_CORS= # ADMIN URL +AUTH_CORS= # STOREFRONT AND ADMIN URLS, SEPARATED BY COMMAS +DISABLE_MEDUSA_ADMIN=false +MEDUSA_WORKER_MODE=server +PORT=9000 +DATABASE_URL # POSTGRES DATABASE URL +REDIS_URL= # REDIS DATABASE URL +``` + +Where: + +- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. +- `STORE_CORS`'s value is the URL of your storefront. If you don’t have it yet, you can skip adding it for now. +- `ADMIN_CORS`'s value is the URL of the admin dashboard, which is the same as the server Medusa application. You can add it later if you don't currently have it. +- `AUTH_CORS`'s value is the URLs of any application authenticating users, customers, or other actor types, such as the storefront and admin URLs. The URLs are separated by commas. If you don’t have the URLs yet, you can set its value later. +- Set `DISABLE_MEDUSA_ADMIN`'s value to `false` so that the admin is built with the server application. +- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` +- Set the Redis database's connection URL as the value of `REDIS_URL`. + +Feel free to add any other relevant environment variables, such as for integrations and architectural modules. + +### Set Start Command + +The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. + +If your hosting provider doesn't support setting a current-working directory, set the start command to the following: + +```bash npm2yarn +cd .medusa/server && npm install && npm run predeploy && npm run start +``` + +Notice that you run the `predeploy` command before starting the Medusa application to run migrations and sync links whenever there's an update. + +### Set Backend URL in Admin Configuration + +After you’ve obtained the Medusa application’s URL, add the following configuration to `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + admin: { + // ... + backendUrl: process.env.MEDUSA_BACKEND_URL, + }, +}) +``` + +Then, push the changes to the GitHub repository or deployed application. + +In your hosting provider, add or modify the following environment variables for the Medusa application in server mode: + +```bash +ADMIN_CORS= # MEDUSA APPLICATION URL +AUTH_CORS= # ADD MEDUSA APPLICATION URL +MEDUSA_BACKEND_URL= # URL TO DEPLOYED MEDUSA APPLICATION +``` + +Where you set the value of `ADMIN_CORS` and `MEDUSA_BACKEND_URL` to the Medusa application’s URL, and you add the URL to `AUTH_CORS`. + +Remember to separate URLs in `AUTH_CORS` by commas. + +*** + +## 6. Deploy Medusa Application in Worker Mode + +Next, you'll deploy the Medusa application in worker mode. + +As explained in the previous section, the deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. + +### Set Environment Variables + +When setting the environment variables of the Medusa application, set the following variables: + +```bash +COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET +JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET +DISABLE_MEDUSA_ADMIN=true +MEDUSA_WORKER_MODE=worker +PORT=9000 +DATABASE_URL # POSTGRES DATABASE URL +REDIS_URL= # REDIS DATABASE URL +``` + +Where: + +- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. +- Set `DISABLE_MEDUSA_ADMIN`'s value to `true` so that the admin isn't built with the worker application. +- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` +- Set the Redis database's connection URL as the value of `REDIS_URL`. + +Feel free to add any other relevant environment variables, such as for integrations and architectural modules. + +### Set Start Command + +The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. + +If your hosting provider doesn't support setting a current-working directory, set the start command to the following: + +```bash npm2yarn +cd .medusa/server && npm run install && npm run start +``` + +*** + +## 7. Test Deployed Application + +Once the application is deployed and live, go to `/health`, where `` is the URL of the Medusa application in server mode. If the deployment was successful, you’ll see the `OK` response. + +The Medusa Admin is also available at `/app`. + +*** + +## Create Admin User + +If your hosting provider supports running commands in your Medusa application's directory, run the following command to create an admin user: + +```bash +npx medusa user -e admin-medusa@test.com -p supersecret +``` + +Replace the email `admin-medusa@test.com` and password `supersecret` with the credentials you want. + +You can use these credentials to log into the Medusa Admin dashboard. + + # Configure Instrumentation In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. @@ -1751,135 +2050,6 @@ 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. - -![Diagram showcasing the flow of a custom developed feature](https://res.cloudinary.com/dza7lstvk/image/upload/v1725867628/Medusa%20Book/custom-development_nofvp6.jpg) - -*** - -## Next Chapters: Brand Module Example - -The next chapters will guide you to: - -1. Build a Brand Module that creates a `Brand` data model and provides data-management features. -2. Add a workflow to create a brand. -3. Expose an API route that allows admin users to create a brand using the workflow. - - -# 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. - -![Diagram showcasing how the Brand Plugin would add its resources to any application it's installed in](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540091/Medusa%20Book/brand-plugin_bk9zi9.jpg) - -Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM. - -To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - - -# Customizations Next Steps: Learn the Fundamentals - -The previous guides introduced Medusa's different concepts and how you can use them to customize Medusa for a realistic use case, You added brands to your application, linked them to products, customized the admin dashboard, and integrated a third-party CMS. - -The next chapters will cover each of these concepts in depth, with the different ways you can use them, their options or configurations, and more advanced features that weren't covered in the previous guides. While you can start building with Medusa, it's highly recommended to follow the next chapters for a better understanding of Medusa's fundamentals. - -## Useful Guides - -The following guides and references are useful for your development journey: - -3. [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md): Browse the list of commerce modules in Medusa and their references to learn how to use them. -4. [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md): Learn about the methods generated by `MedusaService` with examples. -5. [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md): Browse the list of core workflows and their hooks that are useful for your customizations. -6. [Admin Injection Zones](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md): Browse the injection zones in the Medusa Admin to learn where you can inject widgets. - -*** - -## More Examples in Recipes - -In the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more. - - -# 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. - - # Admin Development In the next chapters, you'll learn more about possible admin customizations. @@ -2033,6 +2203,110 @@ curl http://localhost:9000/hello-world You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application. +# Data Models + +In this chapter, you'll learn what a data model is and how to create a data model. + +## What is a Data Model? + +A data model represents a table in the database. You create data models using Medusa's data modeling language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. + +You create a data model in a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). The module's service provides the methods to store and manage those data models. Then, you can resolve the module's service in other customizations, such as a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), to manage the data models' records. + +*** + +## How to Create a Data Model + +In a module, you can create a data model in a TypeScript or JavaScript file under the module's `models` directory. + +So, for example, assuming you have a Blog Module at `src/modules/blog`, you can create a `Post` data model by creating the `src/modules/blog/models/post.ts` file with the following content: + +![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg) + +```ts title="src/modules/blog/models/post.ts" +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + id: model.id().primaryKey(), + title: model.text(), +}) + +export default Post +``` + +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. + +The code snippet above defines a `Post` data model with `id` and `title` properties. + +*** + +## Generate Migrations + +After you create a data model in a module, then [register that module in your Medusa configurations](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you must generate a migration to create the data model's table in the database. + +A migration is a TypeScript or JavaScript file that defines database changes made by a module. 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. + +For example, to generate a migration for the Blog Module, run the following command in your Medusa application's directory: + +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. + +```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: + +```ts +import { Migration } from "@mikro-orm/migrations" + +export class Migration20241121103722 extends Migration { + + async up(): Promise { + 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 { + this.addSql("drop table if exists \"post\" cascade;") + } + +} +``` + +In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table. + +### Run Migrations + +To reflect the changes in the generated migration file on the database, run the `db:migrate` command: + +If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in. + +```bash +npx medusa db:migrate +``` + +This creates the `post` table in the database. + +### Migrations on Data Model Changes + +Whenever you make a change to a data model, you must generate and run the migrations. + +For example, if you add a new column to the `Post` data model, you must generate a new migration and run it. + +*** + +## Manage Data Models + +Your module's service should extend the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), which generates data-management methods for your module's data models. + +For example, the Blog Module's service would have methods like `retrievePost` and `createPosts`. + +Refer to the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter to learn more about how to extend the service factory and manage data models, and refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for the full list of generated methods and how to use them. + + # Environment Variables In this chapter, you'll learn how environment variables are loaded in Medusa. @@ -2364,110 +2638,6 @@ A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), Learn more about the module's container in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md). -# Data Models - -In this chapter, you'll learn what a data model is and how to create a data model. - -## What is a Data Model? - -A data model represents a table in the database. You create data models using Medusa's data modeling language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. - -You create a data model in a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). The module's service provides the methods to store and manage those data models. Then, you can resolve the module's service in other customizations, such as a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), to manage the data models' records. - -*** - -## How to Create a Data Model - -In a module, you can create a data model in a TypeScript or JavaScript file under the module's `models` directory. - -So, for example, assuming you have a Blog Module at `src/modules/blog`, you can create a `Post` data model by creating the `src/modules/blog/models/post.ts` file with the following content: - -![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg) - -```ts title="src/modules/blog/models/post.ts" -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - id: model.id().primaryKey(), - title: model.text(), -}) - -export default Post -``` - -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. - -The code snippet above defines a `Post` data model with `id` and `title` properties. - -*** - -## Generate Migrations - -After you create a data model in a module, then [register that module in your Medusa configurations](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you must generate a migration to create the data model's table in the database. - -A migration is a TypeScript or JavaScript file that defines database changes made by a module. 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. - -For example, to generate a migration for the Blog Module, run the following command in your Medusa application's directory: - -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. - -```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: - -```ts -import { Migration } from "@mikro-orm/migrations" - -export class Migration20241121103722 extends Migration { - - async up(): Promise { - 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 { - this.addSql("drop table if exists \"post\" cascade;") - } - -} -``` - -In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table. - -### Run Migrations - -To reflect the changes in the generated migration file on the database, run the `db:migrate` command: - -If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in. - -```bash -npx medusa db:migrate -``` - -This creates the `post` table in the database. - -### Migrations on Data Model Changes - -Whenever you make a change to a data model, you must generate and run the migrations. - -For example, if you add a new column to the `Post` data model, you must generate a new migration and run it. - -*** - -## Manage Data Models - -Your module's service should extend the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), which generates data-management methods for your module's data models. - -For example, the Blog Module's service would have methods like `retrievePost` and `createPosts`. - -Refer to the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter to learn more about how to extend the service factory and manage data models, and refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for the full list of generated methods and how to use them. - - # Module Link In this chapter, you’ll learn what a module link is and how to define one. @@ -2721,6 +2891,100 @@ For example, in a plugin, you can define a module that integrates a third-party The next chapter explains how you can create and publish a plugin. +# 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: + +![Example of scheduled job file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866423/Medusa%20Book/scheduled-job-dir-overview_ediqgm.jpg) + +```ts title="src/jobs/hello-world.ts" highlights={highlights} +import { MedusaContainer } from "@medusajs/framework/types" + +export default async function greetingJob(container: MedusaContainer) { + const logger = container.resolve("logger") + + logger.info("Greeting!") +} + +export const config = { + name: "greeting-every-minute", + schedule: "* * * * *", +} +``` + +You export an asynchronous function that receives the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) as a parameter. In the function, you resolve the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) from the Medusa container and log a message. + +You also export a `config` object that has the following properties: + +- `name`: A unique name for the job. +- `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. + +This scheduled job executes every minute and logs into the terminal `Greeting!`. + +### Test the Scheduled Job + +To test out your scheduled job, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +After a minute, the following message will be logged to the terminal: + +```bash +info: Greeting! +``` + +*** + +## Example: Sync Products Once a Day + +In this section, you'll find a brief example of how you use a scheduled job to sync products to a third-party service. + +When implementing flows spanning across systems or [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), you use [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). A workflow is a task made up of a series of steps, and you construct it like you would a regular function, but it's a special function that supports rollback mechanism in case of errors, background execution, and more. + +You can learn how to create a workflow in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), but this example assumes you already have a `syncProductToErpWorkflow` implemented. To execute this workflow once a day, create a scheduled job at `src/jobs/sync-products.ts` with the following content: + +```ts title="src/jobs/sync-products.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp" + +export default async function syncProductsJob(container: MedusaContainer) { + await syncProductToErpWorkflow(container) + .run() +} + +export const config = { + name: "sync-products-job", + schedule: "0 0 * * *", +} +``` + +In the scheduled job function, you execute the `syncProductToErpWorkflow` by invoking it and passing it the container, then invoking the `run` method. You also specify in the exported configurations the schedule `0 0 * * *` which indicates midnight time of every day. + +The next time you start the Medusa application, it will run this job every day at midnight. + + # Modules In this chapter, you’ll learn about modules and how to create them. @@ -3021,472 +3285,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. -# 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/pricing) - -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 worker mode in the [Worker Module chapter](https://docs.medusajs.com/learn/production/worker-mode/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 - -The `redisUrl` configuration specifies the connection URL to Redis to store the Medusa server's session. - -Learn more in the [Medusa Configuration documentation](https://docs.medusajs.com/learn/configurations/medusa-config#redisurl/index.html.md). - -So, add the following configuration in `medusa-config.ts` : - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - // ... - redisUrl: process.env.REDIS_URL, - }, -}) -``` - -*** - -## 2. Add predeploy Script - -Before you start the Medusa application in production, you should always run migrations and sync links. - -So, add the following script in `package.json`: - -```json -"scripts": { - // ... - "predeploy": "medusa db:migrate" -}, -``` - -*** - -## 3. Install Production Modules and Providers - -By default, your Medusa application uses modules and providers useful for development, such as the In-Memory Cache Module or the Local File Module Provider. - -It’s highly recommended to instead use modules and providers suitable for production, including: - -- [Redis Cache Module](https://docs.medusajs.com/resources/architectural-modules/cache/redis/index.html.md) -- [Redis Event Bus Module](https://docs.medusajs.com/resources/architectural-modules/event/redis/index.html.md) -- [Workflow Engine Redis Module](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/redis/index.html.md) -- [S3 File Module Provider](https://docs.medusajs.com/resources/architectural-modules/file/s3/index.html.md) (or other file module providers production-ready). -- [SendGrid Notification Module Provider](https://docs.medusajs.com/resources/architectural-modules/notification/sendgrid/index.html.md) (or other notification module providers production-ready). - -Then, add these modules in `medusa-config.ts`: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/cache-redis", - options: { - redisUrl: process.env.REDIS_URL, - }, - }, - { - resolve: "@medusajs/medusa/event-bus-redis", - options: { - redisUrl: process.env.REDIS_URL, - }, - }, - { - resolve: "@medusajs/medusa/workflow-engine-redis", - options: { - redis: { - url: process.env.REDIS_URL, - }, - }, - }, - ], -}) -``` - -Check out the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) and [Architectural Modules](https://docs.medusajs.com/resources/architectural-modules/index.html.md) documentation for other modules and providers to use. - -*** - -## 4. Create PostgreSQL and Redis Databases - -Your Medusa application must connect to PostgreSQL and Redis databases. So, before you deploy it, create production PostgreSQL and Redis databases. - -If your hosting provider doesn't support databases, you can use [Neon for PostgreSQL database hosting](https://neon.tech/), and [Redis Cloud for the Redis database hosting](https://redis.io/cloud/). - -After hosting both databases, keep their connection URLs for the next steps. - -*** - -## 5. Deploy Medusa Application in Server Mode - -As mentioned earlier, you'll deploy two instances or create two deployments of your Medusa application: one in server mode, and the other in worker mode. - -The deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. - -### Set Environment Variables - -When setting the environment variables of the Medusa application, set the following variables: - -```bash -COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET -JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET -STORE_CORS= # STOREFRONT URL -ADMIN_CORS= # ADMIN URL -AUTH_CORS= # STOREFRONT AND ADMIN URLS, SEPARATED BY COMMAS -DISABLE_MEDUSA_ADMIN=false -MEDUSA_WORKER_MODE=server -PORT=9000 -DATABASE_URL # POSTGRES DATABASE URL -REDIS_URL= # REDIS DATABASE URL -``` - -Where: - -- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. -- `STORE_CORS`'s value is the URL of your storefront. If you don’t have it yet, you can skip adding it for now. -- `ADMIN_CORS`'s value is the URL of the admin dashboard, which is the same as the server Medusa application. You can add it later if you don't currently have it. -- `AUTH_CORS`'s value is the URLs of any application authenticating users, customers, or other actor types, such as the storefront and admin URLs. The URLs are separated by commas. If you don’t have the URLs yet, you can set its value later. -- Set `DISABLE_MEDUSA_ADMIN`'s value to `false` so that the admin is built with the server application. -- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` -- Set the Redis database's connection URL as the value of `REDIS_URL`. - -Feel free to add any other relevant environment variables, such as for integrations and architectural modules. - -### Set Start Command - -The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. - -If your hosting provider doesn't support setting a current-working directory, set the start command to the following: - -```bash npm2yarn -cd .medusa/server && npm install && npm run predeploy && npm run start -``` - -Notice that you run the `predeploy` command before starting the Medusa application to run migrations and sync links whenever there's an update. - -### Set Backend URL in Admin Configuration - -After you’ve obtained the Medusa application’s URL, add the following configuration to `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - admin: { - // ... - backendUrl: process.env.MEDUSA_BACKEND_URL, - }, -}) -``` - -Then, push the changes to the GitHub repository or deployed application. - -In your hosting provider, add or modify the following environment variables for the Medusa application in server mode: - -```bash -ADMIN_CORS= # MEDUSA APPLICATION URL -AUTH_CORS= # ADD MEDUSA APPLICATION URL -MEDUSA_BACKEND_URL= # URL TO DEPLOYED MEDUSA APPLICATION -``` - -Where you set the value of `ADMIN_CORS` and `MEDUSA_BACKEND_URL` to the Medusa application’s URL, and you add the URL to `AUTH_CORS`. - -Remember to separate URLs in `AUTH_CORS` by commas. - -*** - -## 6. Deploy Medusa Application in Worker Mode - -Next, you'll deploy the Medusa application in worker mode. - -As explained in the previous section, the deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. - -### Set Environment Variables - -When setting the environment variables of the Medusa application, set the following variables: - -```bash -COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET -JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET -DISABLE_MEDUSA_ADMIN=true -MEDUSA_WORKER_MODE=worker -PORT=9000 -DATABASE_URL # POSTGRES DATABASE URL -REDIS_URL= # REDIS DATABASE URL -``` - -Where: - -- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. -- Set `DISABLE_MEDUSA_ADMIN`'s value to `true` so that the admin isn't built with the worker application. -- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` -- Set the Redis database's connection URL as the value of `REDIS_URL`. - -Feel free to add any other relevant environment variables, such as for integrations and architectural modules. - -### Set Start Command - -The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. - -If your hosting provider doesn't support setting a current-working directory, set the start command to the following: - -```bash npm2yarn -cd .medusa/server && npm run install && npm run start -``` - -*** - -## 7. Test Deployed Application - -Once the application is deployed and live, go to `/health`, where `` is the URL of the Medusa application in server mode. If the deployment was successful, you’ll see the `OK` response. - -The Medusa Admin is also available at `/app`. - -*** - -## Create Admin User - -If your hosting provider supports running commands in your Medusa application's directory, run the following command to create an admin user: - -```bash -npx medusa user -e admin-medusa@test.com -p supersecret -``` - -Replace the email `admin-medusa@test.com` and password `supersecret` with the credentials you want. - -You can use these credentials to log into the Medusa Admin dashboard. - - -# 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: - -![Example of scheduled job file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866423/Medusa%20Book/scheduled-job-dir-overview_ediqgm.jpg) - -```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. - - -# Medusa's Architecture - -In this chapter, you'll learn about the architectural layers in Medusa. - -Find the full architectural diagram at the [end of this chapter](#full-diagram-of-medusas-architecture). - -## 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. The Medusa server is based on Express.js, which handles incoming requests. It can also connect to a Redis database that stores the server session data. -2. Workflows: API Routes consume workflows that hold the opinionated business logic of your application. -3. Modules: Workflows use domain-specific modules for resource management. -4. Data store: Modules query the underlying datastore, which is a PostgreSQL database in common cases. - -These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - -![This diagram illustrates the entry point of requests into the Medusa application through API routes. It shows a storefront and an admin that can send a request to the HTTP layer. The HTTP layer then uses workflows to handle the business logic. Finally, the workflows use modules to query and manipulate data in the data stores.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) - -*** - -## Database Layer - -The Medusa application injects into each module, including your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. - -Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - -![This diagram illustrates how modules connect to the database.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) - -*** - -## Third-Party Integrations Layer - -Third-party services and systems are integrated through Medusa's Commerce and Architectural modules. You also create custom third-party integrations through a [custom module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -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 can integrate [Stripe](https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe/index.html.md) through a Payment Module Provider, or [ShipStation](https://docs.medusajs.com/resources/integrations/guides/shipstation/index.html.md) through a Fulfillment Module Provider. - -You can also integrate third-party services for custom functionalities. For example, you can integrate [Sanity](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md) for rich CMS capabilities, or [Odoo](https://docs.medusajs.com/resources/recipes/erp/odoo/index.html.md) to sync your Medusa application with your ERP system. - -You can replace any of the third-party services mentioned above to build your preferred commerce ecosystem. - -![Diagram illustrating the commerce modules integration to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175357/Medusa%20Book/service-commerce_qcbdsl.jpg) - -### Architectural Modules - -[Architectural modules](https://docs.medusajs.com/resources/architectural-modules/index.html.md) integrate third-party services and systems for architectural features. Medusa has the following Architectural modules: - -- [Cache Module](https://docs.medusajs.com/resources/architectural-modules/cache/index.html.md): Caches data that require heavy computation. You can integrate a custom module to handle the caching with services like Memcached, or use the existing [Redis Cache Module](https://docs.medusajs.com/resources/architectural-modules/cache/redis/index.html.md). -- [Event Module](https://docs.medusajs.com/resources/architectural-modules/event/index.html.md): A pub/sub system that allows you to subscribe to events and trigger them. You can integrate [Redis](https://docs.medusajs.com/resources/architectural-modules/event/redis/index.html.md) as the pub/sub system. -- [File Module](https://docs.medusajs.com/resources/architectural-modules/file/index.html.md): Manages file uploads and storage, such as upload of product images. You can integrate [AWS S3](https://docs.medusajs.com/resources/architectural-modules/file/s3/index.html.md) for file storage. -- [Locking Module](https://docs.medusajs.com/resources/architectural-modules/locking/index.html.md): Manages access to shared resources by multiple processes or threads, preventing conflict between processes and ensuring data consistency. You can integrate [Redis](https://docs.medusajs.com/resources/architectural-modules/locking/redis/index.html.md) for locking. -- [Notification Module](https://docs.medusajs.com/resources/architectural-modules/notification/index.html.md): Sends notifications to customers and users, such as for order updates or newsletters. You can integrate [SendGrid](https://docs.medusajs.com/resources/architectural-modules/notification/sendgrid/index.html.md) for sending emails. -- [Workflow Engine Module](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/index.html.md): Orchestrates workflows that hold the business logic of your application. You can integrate [Redis](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/redis/index.html.md) to orchestrate workflows. - -All of the third-party services mentioned above can be replaced to help you build your preferred architecture and ecosystem. - -![Diagram illustrating the architectural modules integration to third-party services and systems](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175342/Medusa%20Book/service-arch_ozvryw.jpg) - -*** - -## Full Diagram of Medusa's Architecture - -The following diagram illustrates Medusa's architecture including all its layers. - -![Full diagram illustrating Medusa's architecture combining all the different layers.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg) - - # Workflows In this chapter, you’ll learn about workflows and how to define and execute them. @@ -3741,6 +3539,208 @@ 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. +# 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. + + +# 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. + +![Diagram showcasing the flow of a custom developed feature](https://res.cloudinary.com/dza7lstvk/image/upload/v1725867628/Medusa%20Book/custom-development_nofvp6.jpg) + +*** + +## Next Chapters: Brand Module Example + +The next chapters will guide you to: + +1. Build a Brand Module that creates a `Brand` data model and provides data-management features. +2. Add a workflow to create a brand. +3. Expose an API route that allows admin users to create a brand using the workflow. + + +# 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. + +Find the full architectural diagram at the [end of this chapter](#full-diagram-of-medusas-architecture). + +## 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. The Medusa server is based on Express.js, which handles incoming requests. It can also connect to a Redis database that stores the server session data. +2. Workflows: API Routes consume workflows that hold the opinionated business logic of your application. +3. Modules: Workflows use domain-specific modules for resource management. +4. Data store: Modules query the underlying datastore, which is a PostgreSQL database in common cases. + +These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + +![This diagram illustrates the entry point of requests into the Medusa application through API routes. It shows a storefront and an admin that can send a request to the HTTP layer. The HTTP layer then uses workflows to handle the business logic. Finally, the workflows use modules to query and manipulate data in the data stores.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) + +*** + +## Database Layer + +The Medusa application injects into each module, including your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. + +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + +![This diagram illustrates how modules connect to the database.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) + +*** + +## Third-Party Integrations Layer + +Third-party services and systems are integrated through Medusa's Commerce and Architectural modules. You also create custom third-party integrations through a [custom module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +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 can integrate [Stripe](https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe/index.html.md) through a Payment Module Provider, or [ShipStation](https://docs.medusajs.com/resources/integrations/guides/shipstation/index.html.md) through a Fulfillment Module Provider. + +You can also integrate third-party services for custom functionalities. For example, you can integrate [Sanity](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md) for rich CMS capabilities, or [Odoo](https://docs.medusajs.com/resources/recipes/erp/odoo/index.html.md) to sync your Medusa application with your ERP system. + +You can replace any of the third-party services mentioned above to build your preferred commerce ecosystem. + +![Diagram illustrating the commerce modules integration to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175357/Medusa%20Book/service-commerce_qcbdsl.jpg) + +### Architectural Modules + +[Architectural modules](https://docs.medusajs.com/resources/architectural-modules/index.html.md) integrate third-party services and systems for architectural features. Medusa has the following Architectural modules: + +- [Cache Module](https://docs.medusajs.com/resources/architectural-modules/cache/index.html.md): Caches data that require heavy computation. You can integrate a custom module to handle the caching with services like Memcached, or use the existing [Redis Cache Module](https://docs.medusajs.com/resources/architectural-modules/cache/redis/index.html.md). +- [Event Module](https://docs.medusajs.com/resources/architectural-modules/event/index.html.md): A pub/sub system that allows you to subscribe to events and trigger them. You can integrate [Redis](https://docs.medusajs.com/resources/architectural-modules/event/redis/index.html.md) as the pub/sub system. +- [File Module](https://docs.medusajs.com/resources/architectural-modules/file/index.html.md): Manages file uploads and storage, such as upload of product images. You can integrate [AWS S3](https://docs.medusajs.com/resources/architectural-modules/file/s3/index.html.md) for file storage. +- [Locking Module](https://docs.medusajs.com/resources/architectural-modules/locking/index.html.md): Manages access to shared resources by multiple processes or threads, preventing conflict between processes and ensuring data consistency. You can integrate [Redis](https://docs.medusajs.com/resources/architectural-modules/locking/redis/index.html.md) for locking. +- [Notification Module](https://docs.medusajs.com/resources/architectural-modules/notification/index.html.md): Sends notifications to customers and users, such as for order updates or newsletters. You can integrate [SendGrid](https://docs.medusajs.com/resources/architectural-modules/notification/sendgrid/index.html.md) for sending emails. +- [Workflow Engine Module](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/index.html.md): Orchestrates workflows that hold the business logic of your application. You can integrate [Redis](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/redis/index.html.md) to orchestrate workflows. + +All of the third-party services mentioned above can be replaced to help you build your preferred architecture and ecosystem. + +![Diagram illustrating the architectural modules integration to third-party services and systems](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175342/Medusa%20Book/service-arch_ozvryw.jpg) + +*** + +## Full Diagram of Medusa's Architecture + +The following diagram illustrates Medusa's architecture including all its layers. + +![Full diagram illustrating Medusa's architecture combining all the different layers.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg) + + +# Customizations Next Steps: Learn the Fundamentals + +The previous guides introduced Medusa's different concepts and how you can use them to customize Medusa for a realistic use case, You added brands to your application, linked them to products, customized the admin dashboard, and integrated a third-party CMS. + +The next chapters will cover each of these concepts in depth, with the different ways you can use them, their options or configurations, and more advanced features that weren't covered in the previous guides. While you can start building with Medusa, it's highly recommended to follow the next chapters for a better understanding of Medusa's fundamentals. + +## Useful Guides + +The following guides and references are useful for your development journey: + +3. [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md): Browse the list of commerce modules in Medusa and their references to learn how to use them. +4. [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md): Learn about the methods generated by `MedusaService` with examples. +5. [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md): Browse the list of core workflows and their hooks that are useful for your customizations. +6. [Admin Injection Zones](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md): Browse the injection zones in the Medusa Admin to learn where you can inject widgets. + +*** + +## More Examples in Recipes + +In the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more. + + +# Re-Use Customizations with Plugins + +In the previous chapters, you've learned important concepts related to creating modules, implementing commerce features in workflows, exposing those features in API routes, customizing the Medusa Admin dashboard with Admin Extensions, and integrating third-party systems. + +You've implemented the brands example within a single Medusa application. However, this approach is not scalable when you want to reuse your customizations across multiple projects. + +To reuse your customizations across multiple Medusa applications, such as implementing brands in different projects, you can create a plugin. A plugin is an NPM package that encapsulates your customizations and can be installed in any Medusa application. Plugins can include modules, workflows, API routes, Admin Extensions, and more. + +![Diagram showcasing how the Brand Plugin would add its resources to any application it's installed in](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540091/Medusa%20Book/brand-plugin_bk9zi9.jpg) + +Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM. + +To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + + # Worker Mode of Medusa Instance In this chapter, you'll learn about the different modes of running a Medusa instance and how to configure the mode. @@ -4071,6 +4071,75 @@ To manage that database, such as changing its name or perform operations on it i The next chapters provide examples of writing integration tests for API routes and workflows. +# Environment Variables in Admin Customizations + +In this chapter, you'll learn how to use environment variables in your admin customizations. + +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). + +## How to Set Environment Variables + +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: + +```plain +VITE_MY_API_KEY=sk_123 +``` + +*** + +## How to Use Environment Variables + +To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object. + +For example: + +```tsx highlights={[["8"]]} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" + +const ProductWidget = () => { + return ( + +
+ API Key: {import.meta.env.VITE_MY_API_KEY} +
+
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +In this example, you display the API key in a widget using `import.meta.env.VITE_MY_API_KEY`. + +### Type Error on import.meta.env + +If you receive a type error on `import.meta.env`, create the file `src/admin/vite-env.d.ts` with the following content: + +```ts title="src/admin/vite-env.d.ts" +/// +``` + +This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables. + +*** + +## Check Node Environment in Admin Customizations + +To check the current environment, Vite exposes two variables: + +- `import.meta.env.DEV`: Returns `true` if the current environment is development. +- `import.meta.env.PROD`: Returns `true` if the current environment is production. + +Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode). + + # 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. @@ -4190,1724 +4259,6 @@ The `moduleIntegrationTestRunner` function creates a database with a random name To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). -# 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. - -An API Route is an endpoint that acts as an entry point for other clients to interact with your Medusa customizations, such as the admin dashboard, storefronts, or third-party systems. - -The 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. - -### Prerequisites - -- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) - -## 1. Create the API Route - -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…). - -Learn more about API routes [in this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/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: - -![Directory structure of the Medusa application after adding the route](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869882/Medusa%20Book/brand-route-dir-overview-2_hjqlnf.jpg) - -```ts title="src/api/admin/brands/route.ts" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - createBrandWorkflow, -} from "../../../workflows/create-brand" - -type PostAdminCreateBrandType = { - name: string -} - -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const { result } = await createBrandWorkflow(req.scope) - .run({ - input: req.validatedBody, - }) - - res.json({ brand: result }) -} -``` - -You export a route handler function with its name (`POST`) being the HTTP method of the API route you're exposing. - -The 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. - -`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`. - -You return a JSON response with the created brand using the `res.json` method. - -*** - -## 2. Create Validation Schema - -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. - -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). - -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: - -![Directory structure of Medusa application after adding validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869806/Medusa%20Book/brand-route-dir-overview-1_yfyjss.jpg) - -```ts title="src/api/admin/brands/validators.ts" -import { z } from "zod" - -export const PostAdminCreateBrand = z.object({ - name: z.string(), -}) -``` - -You export a validation schema that expects in the request body an object having a `name` property whose value is a string. - -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" - -type PostAdminCreateBrandType = z.infer - -// ... -``` - -*** - -## 3. Add Validation Middleware - -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. - -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. - -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: - -![Directory structure of the Medusa application after adding the middleware](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869977/Medusa%20Book/brand-route-dir-overview-3_kcx511.jpg) - -```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 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. - -In the middleware object, you define three properties: - -- `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. - -*** - -## Test API Route - -To test out the API route, start the Medusa application with the following command: - -```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 returns the created brand in the response: - -```json title="Example Response" -{ - "brand": { - "id": "01J7AX9ES4X113HKY6C681KDZJ", - "name": "Acme", - "created_at": "2024-09-09T08:09:34.244Z", - "updated_at": "2024-09-09T08:09:34.244Z" - } -} -``` - -*** - -## Summary - -By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand. You did that by: - -1. Creating a module that defines and manages a `brand` table in the database. -2. Creating a workflow that uses the module's service to create a brand record, and implements the compensation logic to delete that brand in case an error occurs. -3. Creating an API route that allows admin users to create a brand. - -*** - -## 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 next chapters, you'll learn how to build associations between data models defined in different modules. - - -# 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: - -![Directory structure in the Medusa project after adding the file for createBrandStep](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869184/Medusa%20Book/brand-workflow-dir-overview-1_fjvf5j.jpg) - -```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. - - -# 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. - -### Prerequisites - -- [Brands Module](https://docs.medusajs.com/learn/customization/custom-features/modules/index.html.md) - -## 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. - -Replace or create the `GET` API route at `src/api/admin/brands/route.ts` with the following: - -```ts title="src/api/admin/brands/route.ts" highlights={apiRouteHighlights} -// 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, - metadata: { count, take, skip } = {}, - } = await query.graph({ - entity: "brand", - ...req.queryConfig, - }) - - res.json({ - brands, - count, - limit: take, - offset: skip, - }) -} -``` - -In the API route, you use Query's `graph` method to retrieve the brands. In the method's object parameter, you spread the `queryConfig` property of the request object. This property holds configurations for pagination and retrieved fields. - -The query configurations are combined from default configurations, which you'll add next, and the request's query parameters: - -- `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. - -When you pass pagination configurations to the `graph` method, the returned object has the pagination's details in a `metadata` property, whose value is an object having the following properties: - -- `count`: The total count of items. -- `take`: The maximum number of items returned in the `data` array. -- `skip`: The number of items skipped before retrieving the returned items. - -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). - -*** - -## 2. Add Default Query Configurations - -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: - -```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() - -export default defineMiddlewares({ - routes: [ - // ... - { - matcher: "/admin/brands", - method: "GET", - middlewares: [ - validateAndTransformQuery( - GetBrandsSchema, - { - defaults: [ - "id", - "name", - "products.*", - ], - isList: true, - } - ), - ], - }, - - ], -}) -``` - -You apply the `validateAndTransformQuery` middleware on the `GET /admin/brands` API route. The middleware accepts two parameters: - -- A [Zod](https://zod.dev/) schema that a request's query parameters must satisfy. Medusa provides `createFindParams` that generates a Zod schema with the following properties: - - `fields`: A comma-separated string indicating the fields to retrieve. - - `limit`: The maximum number of items to retrieve. - - `offset`: The number of items to skip before retrieving the returned items. - - `order`: The name of the field to sort the items by. Learn more about sorting in [the API reference](https://docs.medusajs.com/api/admin#sort-order) -- An object of Query configurations having the following properties: - - `defaults`: An array of default fields and relations to retrieve. - - `isList`: Whether the API route returns a list of items. - -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. - -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). - -*** - -## 3. Initialize JS SDK - -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: - -![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg) - -```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). - -*** - -## 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: - -![Directory structure of the Medusa application after adding the UI route.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733472011/Medusa%20Book/brands-admin-dir-overview-3_syytld.jpg) - -```tsx title="src/admin/routes/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 - - return ( - - {/* TODO show brands */} - - ) -} - -export const config = defineRouteConfig({ - label: "Brands", - icon: TagSolid, -}) - -export default BrandsPage -``` - -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. - -First, add the following type in `src/admin/routes/brands/page.tsx`: - -```tsx title="src/admin/routes/brands/page.tsx" -type Brand = { - id: string - name: string -} -type BrandsResponse = { - brands: Brand[] - count: number - limit: number - offset: number -} -``` - -You define the type for a brand, and the type of expected response from the `GET /admin/brands` API route. - -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`: - -```tsx title="src/admin/routes/brands/page.tsx" -import { - // ... - Heading, - createDataTableColumnHelper, - DataTable, - DataTablePaginationState, - useDataTable, -} from "@medusajs/ui" -``` - -You import the `DataTable` component and the following utilities: - -- `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. - -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: - -```tsx title="src/admin/routes/brands/page.tsx" -const columnHelper = createDataTableColumnHelper() - -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. - -Then, replace the `// TODO retrieve brands` in the component with the following: - -```tsx title="src/admin/routes/brands/page.tsx" highlights={queryHighlights} -const limit = 15 -const [pagination, setPagination] = useState({ - pageSize: limit, - pageIndex: 0, -}) -const offset = useMemo(() => { - return pagination.pageIndex * limit -}, [pagination]) - -const { data, isLoading } = useQuery({ - queryFn: () => sdk.client.fetch(`/admin/brands`, { - query: { - limit, - offset, - }, - }), - queryKey: [["brands", limit, offset]], -}) - -// TODO configure data table -``` - -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: - -- `pageSize`: The maximum number of items per page. You set it to `15`. -- `pageIndex`: A zero-based index of the current page of items. - -You also define a memoized `offset` value that indicates the number of items to skip before retrieving the current page's items. - -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. - -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. - -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. - -### Display Brands Table - -Finally, you'll display the brands in a data table. Replace the `// TODO configure data table` in the component with the following: - -```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, - }, -}) -``` - -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. - -Then, replace the `{/* TODO show brands */}` in the return statement with the following: - -```tsx title="src/admin/routes/brands/page.tsx" - - - Brands - - - - -``` - -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. - -*** - -## Test it Out - -To test out the UI route, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, you'll find a new "Brands" sidebar item. Click on it to see the brands in your store. You can also go to `http://localhost:9000/app/brands` to see the page. - -![A new sidebar item is added for the new brands UI route. The UI route shows the table of brands with pagination.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733421074/Medusa%20Book/Screenshot_2024-12-05_at_7.46.52_PM_slcdqd.png) - -*** - -## Summary - -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. - -*** - -## Next Steps: Integrate Third-Party Systems - -Your customizations often span across systems, where you need to retrieve data or perform operations in a third-party system. - -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: 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. - -![Directory structure in Medusa project after adding the brand directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868844/Medusa%20Book/brand-dir-overview-1_hxwvgx.jpg) - -*** - -## 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: - -![Directory structure in module after adding the brand data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868920/Medusa%20Book/brand-dir-overview-2_lexhdl.jpg) - -```ts title="src/modules/brand/models/brand.ts" -import { model } from "@medusajs/framework/utils" - -export const Brand = model.define("brand", { - id: model.id().primaryKey(), - name: model.text(), -}) -``` - -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: - -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 about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties/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). - -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: - -![Directory structure in module after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868984/Medusa%20Book/brand-dir-overview-3_jo7baj.jpg) - -```ts title="src/modules/brand/service.ts" highlights={serviceHighlights} -import { MedusaService } from "@medusajs/framework/utils" -import { Brand } from "./models/brand" - -class BrandModuleService extends MedusaService({ - Brand, -}) { - -} - -export default BrandModuleService -``` - -The `BrandModuleService` extends a 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: - -![Directory structure in module after adding the definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869045/Medusa%20Book/brand-dir-overview-4_nf8ymw.jpg) - -```ts title="src/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). - -[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: - -```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. - -*** - -## 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. - - -# Guide: Add Product's Brand Widget in Admin - -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. - -### Prerequisites - -- [Brands linked to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) - -## 1. Initialize JS SDK - -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. - -So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content: - -![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg) - -```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: - -![Directory structure of the Medusa application after adding the widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414684/Medusa%20Book/brands-admin-dir-overview-2_eq5xhi.jpg) - -```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) => { - const { data: queryResult } = useQuery({ - queryFn: () => sdk.admin.product.retrieve(product.id, { - fields: "+brand.*", - }), - queryKey: [["product", product.id]], - }) - const brandName = (queryResult?.product as AdminProductBrand)?.brand?.name - - return ( - -
-
- Brand -
-
-
- - Name - - - - {brandName || "-"} - -
-
- ) -} - -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. - -![The widget is added as the first section of the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414415/Medusa%20Book/Screenshot_2024-12-05_at_5.59.25_PM_y85m14.png) - -*** - -## 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: 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: - -![Directory structure after creating the hook's file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733384338/Medusa%20Book/brands-hook-dir-overview_ltwr5h.jpg) - -```ts title="src/workflows/hooks/created-product.ts" highlights={hook1Highlights} -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" -import { StepResponse } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" -import { LinkDefinition } from "@medusajs/framework/types" -import { BRAND_MODULE } from "../../modules/brand" -import BrandModuleService from "../../modules/brand/service" - -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`. - -![Diagram showcasing how the order of defining a link affects creating the link](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386156/Medusa%20Book/remote-link-brand-product-exp_fhjmg4.jpg) - -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: - -![Directory structure after adding the middelwares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386868/Medusa%20Book/brands-middleware-dir-overview_uczos1.jpg) - -```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. - - -# Guide: Query Product's Brands - -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 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](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. - -Learn more about selecting fields and relations in the [API Reference](https://docs.medusajs.com/api/admin#select-fields-and-relations). - -For example, send the following request to retrieve the list of products with their brands: - -```bash -curl 'http://localhost:9000/admin/products?fields=+brand.*' \ ---header 'Authorization: Bearer {token}' -``` - -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: - -```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: 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: - -![The directory structure of the Medusa application after adding the link.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733329897/Medusa%20Book/brands-link-dir-overview_t1rhlp.jpg) - -```ts title="src/links/product-brand.ts" highlights={highlights} -import BrandModule from "../modules/brand" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - { - linkable: ProductModule.linkable.product, - isList: true, - }, - BrandModule.linkable.brand -) -``` - -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: - -![Directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493547/Medusa%20Book/cms-dir-overview-4_u5t0ug.jpg) - -```ts title="src/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 -} - -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: - -![Directory structure of the Medusa application after adding the subscriber](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493774/Medusa%20Book/cms-dir-overview-5_iqqwvg.jpg) - -```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. @@ -5953,674 +4304,6 @@ export const config = defineWidgetConfig({ ``` -# 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. - -![Directory structure after adding the directory for the CMS Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492447/Medusa%20Book/cms-dir-overview-1_gasguk.jpg) - -*** - -## 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: - -![Directory structure after adding the CMS Module's service](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492583/Medusa%20Book/cms-dir-overview-2_zwcwh3.jpg) - -```ts title="src/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 - this.options_ = options - - // TODO initialize SDK - } -} - -export default CmsModuleService -``` - -You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters: - -1. The module's container. Since a module is [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), it has a [local container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) different than the Medusa container you use in other customizations. This container holds framework tools like the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) and resources within the module. -2. Options passed to the module when it's later added in Medusa's configurations. These options are useful to pass secret keys or configurations that ensure your module is re-usable across applications. For the CMS Module, you accept the API key to connect to the dummy CMS as an option. - -When integrating a third-party system that has a Node.js SDK or client, you can initialize that client in the constructor to be used in the service's methods. - -### Integration Methods - -Next, you'll add methods that simulate sending requests to a third-party CMS. You'll use these methods later to sync brands from and to the CMS. - -Add the following methods in the `CmsModuleService`: - -```ts title="src/modules/cms/service.ts" highlights={methodsHighlights} -export class CmsModuleService { - // ... - - // a dummy method to simulate sending a request, - // in a realistic scenario, you'd use an SDK, fetch, or axios clients - private async sendRequest(url: string, method: string, data?: any) { - this.logger_.info(`Sending a ${method} request to ${url}.`) - this.logger_.info(`Request Data: ${JSON.stringify(data, null, 2)}`) - this.logger_.info(`API Key: ${JSON.stringify(this.options_.apiKey, null, 2)}`) - } - - async createBrand(brand: Record) { - await this.sendRequest("/brands", "POST", brand) - } - - async deleteBrand(id: string) { - await this.sendRequest(`/brands/${id}`, "DELETE") - } - - async retrieveBrands(): Promise[]> { - await this.sendRequest("/brands", "GET") - - return [] - } -} -``` - -The `sendRequest` method sends requests to the third-party CMS. Since this guide isn't using a real CMS, it only simulates the sending by logging messages in the terminal. - -You also add three methods that use the `sendRequest` method: - -- `createBrand` that creates a brand in the third-party system. -- `deleteBrand` that deletes the brand in the third-party system. -- `retrieveBrands` to retrieve a brand from the third-party system. - -*** - -## 3. Export Module Definition - -After creating the module's service, you'll export the module definition indicating the module's name and service. - -Create the file `src/modules/cms/index.ts` with the following content: - -![Directory structure of the Medusa application after adding the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492991/Medusa%20Book/cms-dir-overview-3_b0byks.jpg) - -```ts title="src/modules/cms/index.ts" -import { Module } from "@medusajs/framework/utils" -import CmsModuleService from "./service" - -export const CMS_MODULE = "cms" - -export default Module(CMS_MODULE, { - service: CmsModuleService, -}) -``` - -You use `Module` from the Modules SDK to export the module's defintion, indicating that the module's name is `cms` and its service is `CmsModuleService`. - -*** - -## 4. Add Module to Medusa's Configurations - -Finally, add the module to the Medusa configurations at `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - // ... - { - resolve: "./src/modules/cms", - options: { - apiKey: process.env.CMS_API_KEY, - }, - }, - ], -}) -``` - -The object passed in `modules` accept an `options` property, whose value is an object of options to pass to the module. These are the options you receive in the `CmsModuleService`'s constructor. - -You can add the `CMS_API_KEY` environment variable to `.env`: - -```bash -CMS_API_KEY=123 -``` - -*** - -## Next Steps: Sync Brand From Medusa to CMS - -You can now use the CMS Module's service to perform actions on the third-party CMS. - -In the next chapter, you'll learn how to emit an event when a brand is created, then handle that event to sync the brand from Medusa to the third-party service. - - -# 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: - -![Directory structure of the Medusa application after creating the file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494196/Medusa%20Book/cms-dir-overview-6_z1omsi.jpg) - -```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), - }) - - const updatedBrands = await brandModuleService.updateBrands(brands) - - return new StepResponse(updatedBrands, prevUpdatedBrands) - }, - async (prevUpdatedBrands, { container }) => { - if (!prevUpdatedBrands) { - return - } - - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) - - await brandModuleService.updateBrands(prevUpdatedBrands) - } -) -``` - -The `updateBrandsStep` receives the brands to update in Medusa. In the step, you retrieve the brand's details in Medusa before the update to pass them to the compensation function. You then update the brands using the Brand Module's `updateBrands` generated method. - -In the compensation function, which receives the brand's old data, you revert the update using the same `updateBrands` method. - -### Create Workflow - -Finally, you'll create the workflow that uses the above steps to sync the brands from the CMS to Medusa. Add to the same `src/workflows/sync-brands-from-cms.ts` file the following: - -```ts title="src/workflows/sync-brands-from-cms.ts" -// other imports... -import { - // ... - createWorkflow, - transform, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -// ... - -export const syncBrandsFromCmsWorkflow = createWorkflow( - "sync-brands-from-system", - () => { - const brands = retrieveBrandsFromCmsStep() - - // TODO create and update brands - } -) -``` - -In the workflow, you only use the `retrieveBrandsFromCmsStep` for now, which retrieves the brands from the third-party CMS. - -Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use [transform](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values. - -Learn more about data manipulation using `transform` in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). - -So, replace the `TODO` with the following: - -```ts title="src/workflows/sync-brands-from-cms.ts" -const { toCreate, toUpdate } = transform( - { - brands, - }, - (data) => { - const toCreate: CreateBrand[] = [] - const toUpdate: UpdateBrand[] = [] - - data.brands.forEach((brand) => { - if (brand.external_id) { - toUpdate.push({ - id: brand.external_id as string, - name: brand.name as string, - }) - } else { - toCreate.push({ - name: brand.name as string, - }) - } - }) - - return { toCreate, toUpdate } - } -) - -// TODO create and update the brands -``` - -`transform` accepts two parameters: - -1. The data to be passed to the function in the second parameter. -2. A function to execute only when the workflow is executed. Its return value can be consumed by the rest of the workflow. - -In `transform`'s function, you loop over the brands array to check which should be created or updated. This logic assumes that a brand in the CMS has an `external_id` property whose value is the brand's ID in Medusa. - -You now have the list of brands to create and update. So, replace the new `TODO` with the following: - -```ts title="src/workflows/sync-brands-from-cms.ts" -const created = createBrandsStep({ brands: toCreate }) -const updated = updateBrandsStep({ brands: toUpdate }) - -return new WorkflowResponse({ - created, - updated, -}) -``` - -You first run the `createBrandsStep` to create the brands that don't exist in Medusa, then the `updateBrandsStep` to update the brands that exist in Medusa. You pass the arrays returned by `transform` as the inputs for the steps. - -Finally, you return an object of the created and updated brands. You'll execute this workflow in the scheduled job next. - -*** - -## 2. Schedule Syncing Task - -You now have the workflow to sync the brands from the CMS to Medusa. Next, you'll create a scheduled job that runs this workflow once a day to ensure the data between Medusa and the CMS are always in sync. - -A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. So, create the file `src/jobs/sync-brands-from-cms.ts` with the following content: - -![Directory structure of the Medusa application after adding the scheduled job](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494592/Medusa%20Book/cms-dir-overview-7_dkjb9s.jpg) - -```ts title="src/jobs/sync-brands-from-cms.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { syncBrandsFromCmsWorkflow } from "../workflows/sync-brands-from-cms" - -export default async function (container: MedusaContainer) { - const logger = container.resolve("logger") - - const { result } = await syncBrandsFromCmsWorkflow(container).run() - - logger.info( - `Synced brands from third-party system: ${ - result.created.length - } brands created and ${result.updated.length} brands updated.`) -} - -export const config = { - name: "sync-brands-from-system", - schedule: "0 0 * * *", // change to * * * * * for debugging -} -``` - -A scheduled job file must export: - -- An asynchronous function that will be executed at the specified schedule. This function must be the file's default export. -- An object of scheduled jobs configuration. It has two properties: - - `name`: A unique name for the scheduled job. - - `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. - -The scheduled job function accepts as a parameter the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) used to resolve framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated. - -Based on the cron expression specified in `config.schedule`, Medusa will run the scheduled job every day at midnight. You can also change it to `* * * * *` to run it every minute for easier debugging. - -*** - -## Test it Out - -To test out the scheduled job, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -If you set the schedule to `* * * * *` for debugging, the scheduled job will run in a minute. You'll see in the logs how many brands were created or updated. - -*** - -## Summary - -By following the previous chapters, you utilized Medusa's framework and orchestration tools to perform and automate tasks that span across systems. - -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. - - -# Environment Variables in Admin Customizations - -In this chapter, you'll learn how to use environment variables in your admin customizations. - -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). - -## How to Set Environment Variables - -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: - -```plain -VITE_MY_API_KEY=sk_123 -``` - -*** - -## How to Use Environment Variables - -To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object. - -For example: - -```tsx highlights={[["8"]]} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" - -const ProductWidget = () => { - return ( - -
- API Key: {import.meta.env.VITE_MY_API_KEY} -
-
- ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -In this example, you display the API key in a widget using `import.meta.env.VITE_MY_API_KEY`. - -### Type Error on import.meta.env - -If you receive a type error on `import.meta.env`, create the file `src/admin/vite-env.d.ts` with the following content: - -```ts title="src/admin/vite-env.d.ts" -/// -``` - -This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables. - -*** - -## Check Node Environment in Admin Customizations - -To check the current environment, Vite exposes two variables: - -- `import.meta.env.DEV`: Returns `true` if the current environment is development. -- `import.meta.env.PROD`: Returns `true` if the current environment is production. - -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", - }, -}) -``` - -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 ( - - {isLoading && Loading...} - {data?.products && ( -
    - {data.products.map((product) => ( -
  • {product.title}
  • - ))} -
- )} -
- ) -} - -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) => { - 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 ( - - - - ) -} - -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/learn/configurations/medusa-config#path/index.html.md) configuration in `medusa-config.ts`. -- `__BACKEND_URL__`: The URL to the Medusa backend, as set in the [admin.backendUrl](https://docs.medusajs.com/learn/configurations/medusa-config#backendurl/index.html.md) configuration in `medusa-config.ts`. -- `__STOREFRONT_URL__`: The URL to the storefront, as set in the [admin.storefrontUrl](https://docs.medusajs.com/learn/configurations/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`. - -*** - -## 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/learn/resources/contribution-guidelines/admin-translations/index.html.md). - - # Admin Routing Customizations 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. @@ -6774,6 +4457,256 @@ export const handle = { 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: + +![Example of widget file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867137/Medusa%20Book/widget-dir-overview_dqsbct.jpg) + +```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" + +// The widget +const ProductWidget = () => { + return ( + +
+ Product Widget +
+
+ ) +} + +// 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) => { + return ( + +
+ + Product Widget {data.title} + +
+
+ ) +} + +// 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 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 ( + + {isLoading && Loading...} + {data?.products && ( +
    + {data.products.map((product) => ( +
  • {product.title}
  • + ))} +
+ )} +
+ ) +} + +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) => { + 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 ( + + + + ) +} + +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/learn/configurations/medusa-config#path/index.html.md) configuration in `medusa-config.ts`. +- `__BACKEND_URL__`: The URL to the Medusa backend, as set in the [admin.backendUrl](https://docs.medusajs.com/learn/configurations/medusa-config#backendurl/index.html.md) configuration in `medusa-config.ts`. +- `__STOREFRONT_URL__`: The URL to the storefront, as set in the [admin.storefrontUrl](https://docs.medusajs.com/learn/configurations/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`. + +*** + +## 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/learn/resources/contribution-guidelines/admin-translations/index.html.md). + + # Admin UI Routes In this chapter, you’ll learn how to create a UI route in the admin dashboard. @@ -7010,125 +4943,6 @@ 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 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: - -![Example of widget file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867137/Medusa%20Book/widget-dir-overview_dqsbct.jpg) - -```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" - -// The widget -const ProductWidget = () => { - return ( - -
- Product Widget -
-
- ) -} - -// 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) => { - return ( - -
- - Product Widget {data.title} - -
-
- ) -} - -// 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. @@ -7317,205 +5131,6 @@ 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. @@ -7735,352 +5350,203 @@ 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. -# Middlewares +# Pass Additional Data to Medusa's API Route -In this chapter, you’ll learn about middlewares and how to create them. +In this chapter, you'll learn how to pass additional data in requests to Medusa's API Route. -## What is a Middleware? +## Why Pass Additional Data? -A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler function. +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. -Middlwares are used to guard API routes, parse request content types other than `application/json`, manipulate request data, and more. +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. -As Medusa's server is based on Express, you can use any [Express middleware](https://expressjs.com/en/resources/middleware.html). +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. -### Middleware Types +### API Routes Accepting Additional Data -There are two types of middlewares: +### API Routes List -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. +- 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 Create a Global Middleware? +## How to Pass Additional Data -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. +### 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: -```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 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`. - -### 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! +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. + *** -## How to Create a Route Middleware? +## Use Additional Data in a Hook -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. +Learn about workflow hooks in [this guide](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). -For example: +Step functions consuming the workflow hook can access the `additional_data` in the first parameter. -```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - MedusaNextFunction, - MedusaRequest, - MedusaResponse, - defineMiddlewares, -} from "@medusajs/framework/http" +For example, consider you want to store the data passed in `additional_data` in the product's `metadata` property. -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom*", - method: ["POST", "PUT"], - middlewares: [ - ( - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - console.log("Received a request!") +To do that, create the file `src/workflows/hooks/product-created.ts` with the following content: - next() +```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 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. +This consumes the `productsCreated` hook, which runs after the products are created. -### Test the Route Middleware +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. -To test the middleware: +### Compensation Function -1. Start the application: +Hooks also accept a compensation function as a second parameter to undo the actions made by the step function. -```bash npm2yarn -npm run dev +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 + ) + } +) ``` -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`. - -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. - -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 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. +This updates the products to their original state before adding the brand to their `metadata` property. # HTTP Methods @@ -8442,6 +5908,354 @@ 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. +# 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 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`. + +### 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 in `src/api/middlewares.ts`, but you specify an additional property `method` in the middleware route object. Its value is one or more HTTP methods to apply the middleware to. + +For example: + +```ts 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`. + +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. + +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 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. @@ -8644,106 +6458,76 @@ 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 +# Add Data Model Check Constraints -In this chapter, you'll learn how to send a response in your API route. +In this chapter, you'll learn how to add check constraints to your data model. -## Send a JSON Response +## What is a Check Constraint? -To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler. +A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown. -For example: - -```ts title="src/api/custom/route.ts" highlights={jsonHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "Hello, World!", - }) -} -``` - -This API route returns the following JSON object: - -```json -{ - "message": "Hello, World!" -} -``` +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. *** -## Set Response Status Code +## How to Set a Check Constraint? -By default, setting the JSON data using the `json` method returns a response with a `200` status code. +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. -To change the status code, use the `status` method of the `MedusaResponse` object. +For example, to set a check constraint on a `price` property that ensures its value can only be a positive number: -For example: +```ts highlights={checks1Highlights} +import { model } from "@medusajs/framework/utils" -```ts title="src/api/custom/route.ts" highlights={statusHighlight} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.status(201).json({ - message: "Hello, World!", - }) -} +const CustomProduct = model.define("custom_product", { + // ... + price: model.bigNumber(), +}) +.checks([ + (columns) => `${columns.price} >= 0`, +]) ``` -The response of this API route has the status code `201`. +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). *** -## Change Response Content Type +## Apply in Migrations -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. +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 create an API route that returns an event stream: +To generate a migration for the data model's module then reflect it on the database, run the following command: -```ts highlights={streamHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }) - - const interval = setInterval(() => { - res.write("Streaming data...\n") - }, 3000) - - req.on("end", () => { - clearInterval(interval) - res.end() - }) -} +```bash +npx medusa db:generate custom_module +npx medusa db:migrate ``` -The `writeHead` method accepts two parameters: - -1. The first one is the response's status code. -2. The second is an object of key-value pairs to set the headers of the response. - -This API route opens a stream by setting the `Content-Type` in the header to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds. - -*** - -## Do More with Responses - -The `MedusaResponse` type is based on [Express's Response](https://expressjs.com/en/api.html#res). Refer to their API reference for other uses of responses. +The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. # Request Body and Query Parameter Validation @@ -8995,244 +6779,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). -# Add Data Model Check Constraints +# API Route Response -In this chapter, you'll learn how to add check constraints to your data model. +In this chapter, you'll learn how to send a response in your API route. -## What is a Check Constraint? +## Send a JSON Response -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. - -Some examples of workflow events: - -1. When a user creates a blog post and you're emitting an event to send a newsletter email. -2. When you finish syncing products to a third-party system and you want to notify the admin user of new products added. -3. When a customer purchases a digital product and you want to generate and send it to them. - -You should only go for a **service event** if you're emitting an event for processes under the hood that don't directly affect front-facing features. - -Some examples of service events: - -1. When you're tracing data manipulation and changes, and you want to track every time some custom data is changed. -2. When you're syncing data with a search engine. - -*** - -## Emit Event in a Workflow - -To emit a workflow event, use the `emitEventStep` helper step provided in the `@medusajs/medusa/core-flows` package. +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 highlights={highlights} -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - emitEventStep, -} from "@medusajs/medusa/core-flows" +```ts title="src/api/custom/route.ts" highlights={jsonHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -const helloWorldWorkflow = createWorkflow( - "hello-world", - () => { - // ... - - emitEventStep({ - eventName: "custom.created", - data: { - id: "123", - // other data payload - }, - }) - } -) +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "Hello, World!", + }) +} ``` -The `emitEventStep` accepts an object having the following properties: +This API route returns the following JSON object: -- `eventName`: The event's name. -- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. - -In this example, you emit the event `custom.created` and pass in the data payload an ID property. - -### Test it Out - -If you execute the workflow, the event is emitted and you can see it in your application's logs. - -Any subscribers listening to the event are executed. +```json +{ + "message": "Hello, World!" +} +``` *** -## Emit Event in a Service +## Set Response Status Code -To emit a service event: +By default, setting the JSON data using the `json` method returns a response with a `200` status code. -1. Resolve `event_bus` from the module's container in your service's constructor: +To change the status code, use the `status` method of the `MedusaResponse` object. -### Extending Service Factory +For example: -```ts title="src/modules/blog/service.ts" highlights={["9"]} -import { IEventBusService } from "@medusajs/framework/types" -import { MedusaService } from "@medusajs/framework/utils" +```ts title="src/api/custom/route.ts" highlights={statusHighlight} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -class BlogModuleService extends MedusaService({ - Post, -}){ - protected eventBusService_: AbstractEventBusModuleService - - constructor({ event_bus }) { - super(...arguments) - this.eventBusService_ = event_bus - } +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.status(201).json({ + message: "Hello, World!", + }) } ``` -### Without Service Factory +The response of this API route has the status code `201`. -```ts title="src/modules/blog/service.ts" highlights={["6"]} -import { IEventBusService } from "@medusajs/framework/types" +*** -class BlogModuleService { - protected eventBusService_: AbstractEventBusModuleService +## Change Response Content Type - constructor({ event_bus }) { - this.eventBusService_ = event_bus - } +To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type. + +For example, to create an API route that returns an event stream: + +```ts highlights={streamHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }) + + const interval = setInterval(() => { + res.write("Streaming data...\n") + }, 3000) + + req.on("end", () => { + clearInterval(interval) + res.end() + }) } ``` -2. Use the event bus service's `emit` method in the service's methods to emit an event: +The `writeHead` method accepts two parameters: -```ts title="src/modules/blog/service.ts" highlights={serviceHighlights} -class BlogModuleService { - // ... - performAction() { - // TODO perform action +1. The first one is the response's status code. +2. The second is an object of key-value pairs to set the headers of the response. - this.eventBusService_.emit({ - name: "custom.event", - data: { - id: "123", - // other data payload - }, - }) - } -} -``` +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. -The method accepts an object having the following properties: +*** -- `name`: The event's name. -- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. +## Do More with Responses -3. By default, the Event Module's service isn't injected into your module's container. To add it to the container, pass it in the module's registration object in `medusa-config.ts` in the `dependencies` property: - -```ts title="medusa-config.ts" highlights={depsHighlight} -import { Modules } from "@medusajs/framework/utils" - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/blog", - dependencies: [ - Modules.EVENT_BUS, - ], - }, - ], -}) -``` - -The `dependencies` property accepts an array of module registration keys. The specified modules' main services are injected into the module's container. - -That's how you can resolve it in your module's main service's constructor. - -### Test it Out - -If you execute the `performAction` method of your service, the event is emitted and you can see it in your application's logs. - -Any subscribers listening to the event are also executed. +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. # Data Model Database Index @@ -9347,51 +6993,6 @@ export default MyCustom This creates a unique composite index on the `name` and `age` properties. -# 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. */} - - # Infer Type of Data Model In this chapter, you'll learn how to infer the type of a data model. @@ -9650,6 +7251,301 @@ const product = await blogModuleService.retrieveProducts( In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. +# Data Model Relationships + +In this chapter, you’ll learn how to define relationships between data models in your module. + +## What is a Relationship Property? + +A relationship property defines an association in the database between two models. It's created using the Data Model Language (DML) methods, such as `hasOne` or `belongsTo`. + +When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type. + +You want to create a relation between data models in the same module. + +You want to create a relationship between data models in different modules. Use module links instead. + +*** + +## One-to-One Relationship + +A one-to-one relationship indicates that one record of a data model belongs to or is associated with another. + +To define a one-to-one relationship, create relationship properties in the data models using the following methods: + +1. `hasOne`: indicates that the model has one record of the specified model. +2. `belongsTo`: indicates that the model belongs to one record of the specified model. + +For example: + +```ts highlights={oneToOneHighlights} +import { model } from "@medusajs/framework/utils" + +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email), +}) + +const Email = model.define("email", { + id: model.id().primaryKey(), + user: model.belongsTo(() => User, { + mappedBy: "email", + }), +}) +``` + +In the example above, a user has one email, and an email belongs to one user. + +The `hasOne` and `belongsTo` methods accept a function as 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. + +### 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/properties#make-property-optional/index.html.md). + +### 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. + +For example: + +```ts highlights={oneToOneUndefinedHighlights} +import { model } from "@medusajs/framework/utils" + +const User = model.define("user", { + id: model.id().primaryKey(), +}) + +const Email = model.define("email", { + id: model.id().primaryKey(), + user: model.belongsTo(() => User, { + mappedBy: undefined, + }), +}) +``` + +### 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. + +![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) + +*** + +## 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. + +For example: + +```ts highlights={oneToManyHighlights} +import { model } from "@medusajs/framework/utils" + +const Store = model.define("store", { + id: model.id().primaryKey(), + products: model.hasMany(() => Product), +}) + +const Product = model.define("product", { + id: model.id().primaryKey(), + store: model.belongsTo(() => Store, { + mappedBy: "products", + }), +}) +``` + +In this example, a store has many products, but a product belongs to one store. + +### Optional Relationship + +To make the relationship optional on the `belongsTo` side, use the `nullable` method on the property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#make-property-optional/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. + +![Diagram illustrating the relation between a store and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733937/Medusa%20Book/one-to-many_d6wtcw.jpg) + +*** + +## 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. + +For example: + +```ts highlights={manyToManyHighlights} +import { model } from "@medusajs/framework/utils" + +const Order = model.define("order", { + id: model.id().primaryKey(), + products: model.manyToMany(() => Product, { + mappedBy: "orders", + pivotTable: "order_product", + joinColumn: "order_id", + inverseJoinColumn: "product_id", + }), +}) + +const Product = model.define("product", { + id: model.id().primaryKey(), + orders: model.manyToMany(() => Order, { + mappedBy: "products", + }), +}) +``` + +The `manyToMany` method accepts two parameters: + +1. A function that returns the associated data model. +2. An object of optional configuration. Only one of the data models in the relation can define the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations, and it's considered the owner data model. The object can accept the following properties: + - `mappedBy`: The name of the relationship property in the other data model. If not set, the property's name is inferred from the associated data model's name. + - `pivotTable`: The name of the pivot table created in the database for the many-to-many relation. If not set, the pivot table is inferred by combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`. + - `joinColumn`: The name of the column in the pivot table that points to the owner model's primary key. + - `inverseJoinColumn`: The name of the column in the pivot table that points to the owned model's primary key. + +The `pivotTable`, `joinColumn`, and `inverseJoinColumn` properties are only available after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). + +Following [Medusa v2.1.0](https://github.com/medusajs/medusa/releases/tag/v2.1.0), if `pivotTable`, `joinColumn`, and `inverseJoinColumn` aren't specified on either model, the owner is decided based on alphabetical order. So, in the example above, the `Order` data model would be the owner. + +In this example, an order is associated with many products, and a product is associated with many orders. Since the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations are defined on the order, it's considered the owner data model. + +### Many-to-Many Relationship in the Database + +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`. + +![Diagram illustrating the relation between order and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726734269/Medusa%20Book/many-to-many_fzy5pq.jpg) + +### 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. + + # Data Model Properties In this chapter, you'll learn about the different property types you can use in a data model and how to configure a data model's properties. @@ -10002,299 +7898,208 @@ const posts = await blogModuleService.listPosts({ This retrieves records that include `New Products` in their `title` property. -# Data Model Relationships +# Migrations -In this chapter, you’ll learn how to define relationships between data models in your module. +In this chapter, you'll learn what a migration is and how to generate a migration or write it manually. -## What is a Relationship Property? +## What is a Migration? -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`. +A migration is a TypeScript or JavaScript file that defines database changes made by a module. 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. -When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type. +The migration's file has a class with two methods: -You want to create a relation between data models in the same module. - -You want to create a relationship between data models in different modules. Use module links instead. +- The `up` method reflects changes on the database. +- The `down` method reverts the changes made in the `up` method. *** -## One-to-One Relationship +## Generate Migration -A one-to-one relationship indicates that one record of a data model belongs to or is associated with another. +Instead of you writing the migration manually, 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 a modules' data models. -To define a one-to-one relationship, create relationship properties in the data models using the following methods: +For example, assuming you have a `blog` Module, you can generate a migration for it by running the following command: -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 highlights={oneToOneHighlights} -import { model } from "@medusajs/framework/utils" - -const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email), -}) - -const Email = model.define("email", { - id: model.id().primaryKey(), - user: model.belongsTo(() => User, { - mappedBy: "email", - }), -}) +```bash +npx medusa db:generate blog ``` -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. - -### 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/properties#make-property-optional/index.html.md). - -### 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. - -For example: - -```ts highlights={oneToOneUndefinedHighlights} -import { model } from "@medusajs/framework/utils" - -const User = model.define("user", { - id: model.id().primaryKey(), -}) - -const Email = model.define("email", { - id: model.id().primaryKey(), - user: model.belongsTo(() => User, { - mappedBy: undefined, - }), -}) -``` - -### 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. - -![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) +This generates a migration file under the `migrations` directory of the Blog Module. You can then run it to reflect the changes in the database as mentioned in [this section](#run-the-migration). *** -## One-to-Many Relationship +## Write a Migration Manually -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. +You can also write migrations manually. To do that, create a file in the `migrations` directory of the module and in it, a class that has an `up` and `down` method. The class's name should be of the format `Migration{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}.ts` to ensure migrations are ran in the correct order. For example: -```ts highlights={oneToManyHighlights} -import { model } from "@medusajs/framework/utils" +```ts title="src/modules/blog/migrations/Migration20240429.ts" +import { Migration } from "@mikro-orm/migrations" -const Store = model.define("store", { - id: model.id().primaryKey(), - products: model.hasMany(() => Product), -}) +export class Migration202507021059 extends Migration { -const Product = model.define("product", { - id: model.id().primaryKey(), - store: model.belongsTo(() => Store, { - mappedBy: "products", - }), -}) + async up(): Promise { + this.addSql("create table if not exists \"author\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"author_pkey\" primary key (\"id\"));") + } + + async down(): Promise { + this.addSql("drop table if exists \"author\" cascade;") + } + +} ``` -In this example, a store has many products, but a product belongs to one store. +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. -### Optional Relationship +In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted. -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/properties#make-property-optional/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. - -![Diagram illustrating the relation between a store and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733937/Medusa%20Book/one-to-many_d6wtcw.jpg) +Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. *** -## Many-to-Many Relationship +## Run the Migration -A many-to-many relationship indicates that many records of a data model can be associated with many records of another data model. +To run your migration, run the following command: -To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method. +This command also syncs module links. If you don't want that, use the `--skip-links` option. -For example: - -```ts highlights={manyToManyHighlights} -import { model } from "@medusajs/framework/utils" - -const Order = model.define("order", { - id: model.id().primaryKey(), - products: model.manyToMany(() => Product, { - mappedBy: "orders", - pivotTable: "order_product", - joinColumn: "order_id", - inverseJoinColumn: "product_id", - }), -}) - -const Product = model.define("product", { - id: model.id().primaryKey(), - orders: model.manyToMany(() => Order, { - mappedBy: "products", - }), -}) +```bash +npx medusa db:migrate ``` -The `manyToMany` method accepts two parameters: - -1. A function that returns the associated data model. -2. An object of optional configuration. Only one of the data models in the relation can define the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations, and it's considered the owner data model. The object can accept the following properties: - - `mappedBy`: The name of the relationship property in the other data model. If not set, the property's name is inferred from the associated data model's name. - - `pivotTable`: The name of the pivot table created in the database for the many-to-many relation. If not set, the pivot table is inferred by combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`. - - `joinColumn`: The name of the column in the pivot table that points to the owner model's primary key. - - `inverseJoinColumn`: The name of the column in the pivot table that points to the owned model's primary key. - -The `pivotTable`, `joinColumn`, and `inverseJoinColumn` properties are only available after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). - -Following [Medusa v2.1.0](https://github.com/medusajs/medusa/releases/tag/v2.1.0), if `pivotTable`, `joinColumn`, and `inverseJoinColumn` aren't specified on either model, the owner is decided based on alphabetical order. So, in the example above, the `Order` data model would be the owner. - -In this example, an order is associated with many products, and a product is associated with many orders. Since the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations are defined on the order, it's considered the owner data model. - -### Many-to-Many Relationship in the Database - -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`. - -![Diagram illustrating the relation between order and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726734269/Medusa%20Book/many-to-many_fzy5pq.jpg) - -### 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. +This reflects the changes in the database as implemented in the migration's `up` method. *** -## Set Relationship Name in the Other Model +## Rollback the Migration -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. +To rollback or revert the last migration you ran for a module, run the following command: -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", - }), -}) +```bash +npx medusa db:rollback blog ``` -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 rolls back the last ran migration on the Blog Module. + +### Caution: Rollback Migration before Deleting + +If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations. + +For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors. + +So, always rollback the migration before deleting it. *** -## Cascades +## More Database Commands -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. +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). -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. +# 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 highlights={highlights} -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 Store = model.define("store", { - id: model.id().primaryKey(), - products: model.hasMany(() => Product), -}) -.cascades({ - delete: ["products"], -}) +export default async function productCreateHandler({ + event, +}: SubscriberArgs<{ id: string }>) { + const productId = event.data.id + console.log(`The product ${productId} was created`) +} -const Product = model.define("product", { - id: model.id().primaryKey(), - store: model.belongsTo(() => Store, { - mappedBy: "products", - }), -}) +export const config: SubscriberConfig = { + event: "product.created", +} ``` -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 `event` object has the following properties: -In the example above, when a store is deleted, its associated products are also deleted. +- 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. */} + + +# 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. + +The details in this chapter don't apply to [Read-Only Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md). Refer to the [Read-Only Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md) for more information on read-only links and their direction. + +## Link Direction + +The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`. + +For example, the following defines a link from the Blog Module's `post` data model to the Product Module's `product` data model: + +```ts +export default defineLink( + BlogModule.linkable.post, + ProductModule.linkable.product +) +``` + +Whereas the following defines a link from the Product Module's `product` data model to the Blog Module's `post` data model: + +```ts +export default defineLink( + ProductModule.linkable.product, + BlogModule.linkable.post +) +``` + +The above links are two different links that serve different purposes. + +*** + +## Which Link Direction to Use? + +### Extend Data Models + +If you're adding a link to a data model to extend it and add new fields, define the link from the main data model to the custom data model. + +For example, 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, + BlogModule.linkable.subtitle +) +``` + +### 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`: + +```ts +export default defineLink( + BlogModule.linkable.post, + ProductModule.linkable.product +) +``` # Add Columns to a Link Table @@ -10455,165 +8260,6 @@ await link.create({ ``` -# Migrations - -In this chapter, you'll learn what a migration is and how to generate a migration or write it manually. - -## What is a Migration? - -A migration is a TypeScript or JavaScript file that defines database changes made by a module. 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. - -The migration's file has a class with two methods: - -- The `up` method reflects changes on the database. -- The `down` method reverts the changes made in the `up` method. - -*** - -## Generate Migration - -Instead of you writing the migration manually, 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 a modules' data models. - -For example, assuming you have a `blog` Module, you can generate a migration for it by running the following command: - -```bash -npx medusa db:generate blog -``` - -This generates a migration file under the `migrations` directory of the Blog Module. You can then run it to reflect the changes in the database as mentioned in [this section](#run-the-migration). - -*** - -## Write a Migration Manually - -You can also write migrations manually. To do that, create a file in the `migrations` directory of the module and in it, a class that has an `up` and `down` method. The class's name should be of the format `Migration{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}.ts` to ensure migrations are ran in the correct order. - -For example: - -```ts title="src/modules/blog/migrations/Migration20240429.ts" -import { Migration } from "@mikro-orm/migrations" - -export class Migration202507021059 extends Migration { - - async up(): Promise { - this.addSql("create table if not exists \"author\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"author_pkey\" primary key (\"id\"));") - } - - async down(): Promise { - this.addSql("drop table if exists \"author\" cascade;") - } - -} -``` - -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 `author`, 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 blog -``` - -This rolls back the last ran migration on the Blog Module. - -### Caution: Rollback Migration before Deleting - -If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations. - -For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors. - -So, always rollback the migration before deleting it. - -*** - -## 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). - - -# 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. - -The details in this chapter don't apply to [Read-Only Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md). Refer to the [Read-Only Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md) for more information on read-only links and their direction. - -## Link Direction - -The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`. - -For example, the following defines a link from the Blog Module's `post` data model to the Product Module's `product` data model: - -```ts -export default defineLink( - BlogModule.linkable.post, - ProductModule.linkable.product -) -``` - -Whereas the following defines a link from the Product Module's `product` data model to the Blog Module's `post` data model: - -```ts -export default defineLink( - ProductModule.linkable.product, - BlogModule.linkable.post -) -``` - -The above links are two different links that serve different purposes. - -*** - -## Which Link Direction to Use? - -### Extend Data Models - -If you're adding a link to a data model to extend it and add new fields, define the link from the main data model to the custom data model. - -For example, 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, - BlogModule.linkable.subtitle -) -``` - -### 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`: - -```ts -export default defineLink( - BlogModule.linkable.post, - ProductModule.linkable.product -) -``` - - # Link In this chapter, you’ll learn what Link is and how to use it to manage links. @@ -10754,6 +8400,400 @@ await link.restore({ ``` +# 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. + +Some examples of workflow events: + +1. When a user creates a blog post and you're emitting an event to send a newsletter email. +2. When you finish syncing products to a third-party system and you want to notify the admin user of new products added. +3. When a customer purchases a digital product and you want to generate and send it to them. + +You should only go for a **service event** if you're emitting an event for processes under the hood that don't directly affect front-facing features. + +Some examples of service events: + +1. When you're tracing data manipulation and changes, and you want to track every time some custom data is changed. +2. When you're syncing data with a search engine. + +*** + +## Emit Event in a Workflow + +To emit a workflow event, use the `emitEventStep` helper step provided in the `@medusajs/medusa/core-flows` package. + +For example: + +```ts highlights={highlights} +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + emitEventStep, +} from "@medusajs/medusa/core-flows" + +const helloWorldWorkflow = createWorkflow( + "hello-world", + () => { + // ... + + emitEventStep({ + eventName: "custom.created", + data: { + id: "123", + // other data payload + }, + }) + } +) +``` + +The `emitEventStep` accepts an object having the following properties: + +- `eventName`: The event's name. +- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. + +In this example, you emit the event `custom.created` and pass in the data payload an ID property. + +### Test it Out + +If you execute the workflow, the event is emitted and you can see it in your application's logs. + +Any subscribers listening to the event are executed. + +*** + +## Emit Event in a Service + +To emit a service event: + +1. Resolve `event_bus` from the module's container in your service's constructor: + +### Extending Service Factory + +```ts title="src/modules/blog/service.ts" highlights={["9"]} +import { IEventBusService } from "@medusajs/framework/types" +import { MedusaService } from "@medusajs/framework/utils" + +class BlogModuleService extends MedusaService({ + Post, +}){ + protected eventBusService_: AbstractEventBusModuleService + + constructor({ event_bus }) { + super(...arguments) + this.eventBusService_ = event_bus + } +} +``` + +### Without Service Factory + +```ts title="src/modules/blog/service.ts" highlights={["6"]} +import { IEventBusService } from "@medusajs/framework/types" + +class BlogModuleService { + protected eventBusService_: AbstractEventBusModuleService + + constructor({ event_bus }) { + this.eventBusService_ = event_bus + } +} +``` + +2. Use the event bus service's `emit` method in the service's methods to emit an event: + +```ts title="src/modules/blog/service.ts" highlights={serviceHighlights} +class BlogModuleService { + // ... + performAction() { + // TODO perform action + + this.eventBusService_.emit({ + name: "custom.event", + data: { + id: "123", + // other data payload + }, + }) + } +} +``` + +The method accepts an object having the following properties: + +- `name`: The event's name. +- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. + +3. By default, the Event Module's service isn't injected into your module's container. To add it to the container, pass it in the module's registration object in `medusa-config.ts` in the `dependencies` property: + +```ts title="medusa-config.ts" highlights={depsHighlight} +import { Modules } from "@medusajs/framework/utils" + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/blog", + dependencies: [ + Modules.EVENT_BUS, + ], + }, + ], +}) +``` + +The `dependencies` property accepts an array of module registration keys. The specified modules' main services are injected into the module's container. + +That's how you can resolve it in your module's main service's constructor. + +### Test it Out + +If you execute the `performAction` method of your service, the event is emitted and you can see it in your application's logs. + +Any subscribers listening to the event are also executed. + + +# Query Context + +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). + +## 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. + +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). + +*** + +## How to Use Query Context + +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`). + +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: + +```ts +const { data } = await query.graph({ + entity: "post", + fields: ["*"], + context: QueryContext({ + lang: "es", + }), +}) +``` + +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: + +```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 | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context + + let posts = await super.listPosts(filters, config, sharedContext) + + if (context.lang === "es") { + posts = posts.map((post) => { + return { + ...post, + title: post.title + " en español", + } + }) + } + + return posts + } +} + +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". + +Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md). + +### Using Pagination with Query + +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: + +```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" + +class BlogModuleService extends MedusaService({ + Post, + Author, +}){ + // @ts-ignore + async listAndCountPosts( + filters?: any, + config?: FindConfig | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context + + const result = await super.listAndCountPosts( + filters, + config, + sharedContext + ) + + if (context.lang === "es") { + result.posts = posts.map((post) => { + return { + ...post, + title: post.title + " en español", + } + }) + } + + return result + } +} + +export default BlogModuleService +``` + +Now, the `listAndCountPosts` method will handle the context passed to `query.graph` when you pass pagination fields. You can also move the logic to transform the posts' titles to a separate method and call it from both `listPosts` and `listAndCountPosts`. + +*** + +## Passing Query Context to Related Data Models + +If you're retrieving a data model and you want to pass context to its associated model in the same module, you can pass them as part of `QueryContext`'s parameter, then handle them in the same `list` method. + +For linked data models, check out the [next section](#passing-query-context-to-linked-data-models). + +For example, to pass a context for the post's authors: + +```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: + +```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 | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context + + let posts = await super.listPosts(filters, config, sharedContext) + + 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, + }, + } + }) + } + + 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. + +*** + +## 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). + + # Query In this chapter, you’ll learn about Query and how to use it to fetch data from modules. @@ -11108,232 +9148,6 @@ Try passing one of the Query configuration parameters, like `fields` or `limit`, 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 - -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). - -## 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. - -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). - -*** - -## How to Use Query Context - -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`). - -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: - -```ts -const { data } = await query.graph({ - entity: "post", - fields: ["*"], - context: QueryContext({ - lang: "es", - }), -}) -``` - -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: - -```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 | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context - - let posts = await super.listPosts(filters, config, sharedContext) - - if (context.lang === "es") { - posts = posts.map((post) => { - return { - ...post, - title: post.title + " en español", - } - }) - } - - return posts - } -} - -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". - -Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md). - -### Using Pagination with Query - -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: - -```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" - -class BlogModuleService extends MedusaService({ - Post, - Author, -}){ - // @ts-ignore - async listAndCountPosts( - filters?: any, - config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context - - const result = await super.listAndCountPosts( - filters, - config, - sharedContext - ) - - if (context.lang === "es") { - result.posts = posts.map((post) => { - return { - ...post, - title: post.title + " en español", - } - }) - } - - return result - } -} - -export default BlogModuleService -``` - -Now, the `listAndCountPosts` method will handle the context passed to `query.graph` when you pass pagination fields. You can also move the logic to transform the posts' titles to a separate method and call it from both `listPosts` and `listAndCountPosts`. - -*** - -## Passing Query Context to Related Data Models - -If you're retrieving a data model and you want to pass context to its associated model in the same module, you can pass them as part of `QueryContext`'s parameter, then handle them in the same `list` method. - -For linked data models, check out the [next section](#passing-query-context-to-linked-data-models). - -For example, to pass a context for the post's authors: - -```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: - -```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 | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context - - let posts = await super.listPosts(filters, config, sharedContext) - - 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, - }, - } - }) - } - - 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. - -*** - -## 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). - - # Read-Only Module Link In this chapter, you’ll learn what a read-only module link is and how to define one. @@ -12182,6 +9996,111 @@ npm publish This will publish an updated version of your plugin under a new version. +# Architectural Modules + +In this chapter, you’ll learn about architectural modules. + +## What is an Architectural Module? + +An architectural module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. + +Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis. + +*** + +## Architectural Module Types + +There are different architectural module types including: + +![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/architectural-modules_bj9bb9.jpg) + +- 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. + + +# 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. + + +# 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. + +*** + +## Use Commerce Modules in Custom Flows + +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, 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) + } +) +``` + +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. + + # Module Container In this chapter, you'll learn about the module's container and how to resolve resources in that container. @@ -12248,79 +10167,348 @@ export default async function helloWorldLoader({ ``` -# Architectural Modules +# Loaders -In this chapter, you’ll learn about architectural modules. +In this chapter, you’ll learn about loaders and how to use them. -## What is an Architectural Module? +## What is a Loader? -An architectural module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. +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. -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. +In Medusa, you can execute an action when the application starts using a loader. A loader is a function exported by a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of business logic for a single domain. When the Medusa application starts, it executes all loaders exported by configured modules. + +Loaders are useful to register custom resources, such as database connections, in the [module's container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md), which is similar to the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) but includes only [resources available to the module](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). Modules are isolated, so they can't access resources outside of them, such as a service in another module. + +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). *** -## Architectural Module Types +## How to Create a Loader? -There are different architectural module types including: +### 1. Implement Loader Function -![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/architectural-modules_bj9bb9.jpg) +You create a loader function in a TypeScript or JavaScript file under a module's `loaders` directory. -- 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. +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: + +![Example of loader file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732865671/Medusa%20Book/loader-dir-overview_eg6vtu.jpg) + +Learn how to create a module in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +```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. *** -## Architectural Modules List +## Example: Register Custom MongoDB Connection -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. +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) { + 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. -# Commerce Modules +# Module Isolation -In this chapter, you'll learn about Medusa's commerce modules. +In this chapter, you'll learn how modules are isolated, and what that means for your custom development. -## What is a Commerce Module? +- 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. -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. +## How are Modules Isolated? -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. +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'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. +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. *** -## Use Commerce Modules in Custom Flows +## Why are Modules Isolated -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. +Some of the module isolation's benefits include: -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: +- 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. -```ts highlights={highlights} -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +*** -export const countProductsStep = createStep( - "count-products", - async ({ }, { container }) => { - const productModuleService = container.resolve("product") +## How to Extend Data Model of Another Module? - const [,count] = await productModuleService.listAndCountProducts() +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). - return new StepResponse(count) +*** + +## How to Use Services of Other Modules? + +If you're building a feature that uses functionalities from different modules, use a workflow whose steps resolve the modules' services to perform these functionalities. + +Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. + +### Example + +For example, consider you have two modules: + +1. A module that stores and manages brands in your application. +2. A module that integrates a third-party Content Management System (CMS). + +To sync brands from your application to the third-party system, create the following steps: + +```ts title="Example Steps" highlights={stepsHighlights} +const retrieveBrandsStep = createStep( + "retrieve-brands", + async (_, { container }) => { + const brandModuleService = container.resolve( + "brandModuleService" + ) + + const brands = await brandModuleService.listBrands() + + return new StepResponse(brands) + } +) + +const createBrandsInCmsStep = createStep( + "create-brands-in-cms", + async ({ brands }, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" + ) + + const cmsBrands = await cmsModuleService.createBrands(brands) + + return new StepResponse(cmsBrands, cmsBrands) + }, + async (brands, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" + ) + + await cmsModuleService.deleteBrands( + brands.map((brand) => brand.id) + ) } ) ``` -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 `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. # Perform Database Operations in a Service @@ -12771,350 +10959,6 @@ The following directories are optional and their content are explained more in t - `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. - -## What is a Loader? - -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. - -In Medusa, you can execute an action when the application starts using a loader. A loader is a function exported by a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of business logic for a single domain. When the Medusa application starts, it executes all loaders exported by configured modules. - -Loaders are useful to register custom resources, such as database connections, in the [module's container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md), which is similar to the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) but includes only [resources available to the module](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). Modules are isolated, so they can't access resources outside of them, such as a service in another module. - -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: - -![Example of loader file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732865671/Medusa%20Book/loader-dir-overview_eg6vtu.jpg) - -Learn how to create a module in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -```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) { - 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. - - -# 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. - -## How are Modules Isolated? - -A module is unaware of any resources other than its own, such as services or data models. This means it can't access these resources if they're implemented in another module. - -For example, your custom module can't resolve the Product Module's main service or have direct relationships from its data model to the Product Module's data models. - -*** - -## Why are Modules Isolated - -Some of the module isolation's benefits include: - -- Integrate your module into any Medusa application without side-effects to your setup. -- Replace existing modules with your custom implementation, if your use case is drastically different. -- Use modules in other environments, such as Edge functions and Next.js apps. - -*** - -## How to Extend Data Model of Another Module? - -To extend the data model of another module, such as the `product` data model of the Product Module, use Medusa's linking concepts as explained in the [Module Links chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). - -*** - -## How to Use Services of Other Modules? - -If you're building a feature that uses functionalities from different modules, use a workflow whose steps resolve the modules' services to perform these functionalities. - -Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. - -### Example - -For example, consider you have two modules: - -1. A module that stores and manages brands in your application. -2. A module that integrates a third-party Content Management System (CMS). - -To sync brands from your application to the third-party system, create the following steps: - -```ts title="Example Steps" highlights={stepsHighlights} -const retrieveBrandsStep = createStep( - "retrieve-brands", - async (_, { container }) => { - const brandModuleService = container.resolve( - "brandModuleService" - ) - - const brands = await brandModuleService.listBrands() - - return new StepResponse(brands) - } -) - -const createBrandsInCmsStep = createStep( - "create-brands-in-cms", - async ({ brands }, { container }) => { - const cmsModuleService = container.resolve( - "cmsModuleService" - ) - - const cmsBrands = await cmsModuleService.createBrands(brands) - - return new StepResponse(cmsBrands, cmsBrands) - }, - async (brands, { container }) => { - const cmsModuleService = container.resolve( - "cmsModuleService" - ) - - await cmsModuleService.deleteBrands( - brands.map((brand) => brand.id) - ) - } -) -``` - -The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module. - -Then, create the following workflow that uses these steps: - -```ts title="Example Workflow" -export const syncBrandsWorkflow = createWorkflow( - "sync-brands", - () => { - const brands = retrieveBrandsStep() - - createBrandsInCmsStep({ brands }) - } -) -``` - -You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. - - # Multiple Services in a Module In this chapter, you'll learn how to use multiple services in a module. @@ -13621,264 +11465,6 @@ export default BlogModuleService ``` -# 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. - - -# 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. - - -# 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. - -## Why If-Conditions Aren't Allowed in Workflows? - -Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. At that point, variables in the workflow don't have any values. They only do when you execute the workflow. - -So, you can't use an if-condition that checks a variable's value, as the condition will be evaluated when Medusa creates the internal representation of the workflow, rather than during execution. - -Instead, use when-then from the Workflows SDK. It allows you to perform steps in a workflow only if a condition that you specify is satisfied. - -Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code. - -*** - -## How to use When-Then? - -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: - -```ts highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - when, -} from "@medusajs/framework/workflows-sdk" -// step imports... - -const workflow = createWorkflow( - "workflow", - function (input: { - is_active: boolean - }) { - - const result = when( - input, - (input) => { - return input.is_active - } - ).then(() => { - const stepResult = isActiveStep() - return stepResult - }) - - // executed without condition - const anotherStepResult = anotherStep(result) - - return new WorkflowResponse( - anotherStepResult - ) - } -) -``` - -In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`. - -### When Parameters - -`when` accepts the following parameters: - -1. The first parameter is either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. -2. The second parameter is a function that returns a boolean indicating whether to execute the action in `then`. - -### Then Parameters - -To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. - -The callback function is only executed if `when`'s second parameter function returns a `true` value. - -*** - -## Implementing If-Else with When-Then - -when-then doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow. - -For example: - -```ts highlights={ifElseHighlights} -const workflow = createWorkflow( - "workflow", - function (input: { - is_active: boolean - }) { - - const isActiveResult = when( - input, - (input) => { - return input.is_active - } - ).then(() => { - return isActiveStep() - }) - - const notIsActiveResult = when( - input, - (input) => { - return !input.is_active - } - ).then(() => { - return notIsActiveStep() - }) - - // ... - } -) -``` - -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. - -*** - -## Specify Name for When-Then - -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: - -```ts -const isActiveResult = when( - input, - (input) => { - return input.is_active - } -).then(() => { - return isActiveStep() -}) -``` - -This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name. - -However, if you need to return in your `when-then` block something other than a step's result, you need to specify a unique step name for that block. Otherwise, Medusa will generate a random name for it which can cause unexpected errors in production. - -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, - } -}) -``` - -Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters: - -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`. - -The second and third parameters are the same as the parameters you previously passed to `when`. - - # Access Workflow Errors In this chapter, you’ll learn how to access errors that occur during a workflow’s execution. @@ -14178,6 +11764,522 @@ The `StepResponse.permanentFailure` fails the step and its workflow, triggering 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. +# 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. + + +# 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. + +## Why If-Conditions Aren't Allowed in Workflows? + +Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. At that point, variables in the workflow don't have any values. They only do when you execute the workflow. + +So, you can't use an if-condition that checks a variable's value, as the condition will be evaluated when Medusa creates the internal representation of the workflow, rather than during execution. + +Instead, use when-then from the Workflows SDK. It allows you to perform steps in a workflow only if a condition that you specify is satisfied. + +Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code. + +*** + +## How to use When-Then? + +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: + +```ts highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + when, +} from "@medusajs/framework/workflows-sdk" +// step imports... + +const workflow = createWorkflow( + "workflow", + function (input: { + is_active: boolean + }) { + + const result = when( + input, + (input) => { + return input.is_active + } + ).then(() => { + const stepResult = isActiveStep() + return stepResult + }) + + // executed without condition + const anotherStepResult = anotherStep(result) + + return new WorkflowResponse( + anotherStepResult + ) + } +) +``` + +In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`. + +### When Parameters + +`when` accepts the following parameters: + +1. The first parameter is either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. +2. The second parameter is a function that returns a boolean indicating whether to execute the action in `then`. + +### Then Parameters + +To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. + +The callback function is only executed if `when`'s second parameter function returns a `true` value. + +*** + +## Implementing If-Else with When-Then + +when-then doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow. + +For example: + +```ts highlights={ifElseHighlights} +const workflow = createWorkflow( + "workflow", + function (input: { + is_active: boolean + }) { + + const isActiveResult = when( + input, + (input) => { + return input.is_active + } + ).then(() => { + return isActiveStep() + }) + + const notIsActiveResult = when( + input, + (input) => { + return !input.is_active + } + ).then(() => { + return notIsActiveStep() + }) + + // ... + } +) +``` + +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. + +*** + +## Specify Name for When-Then + +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: + +```ts +const isActiveResult = when( + input, + (input) => { + return input.is_active + } +).then(() => { + return isActiveStep() +}) +``` + +This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name. + +However, if you need to return in your `when-then` block something other than a step's result, you need to specify a unique step name for that block. Otherwise, Medusa will generate a random name for it which can cause unexpected errors in production. + +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, + } +}) +``` + +Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters: + +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`. + +The second and third parameters are the same as the parameters you previously passed to `when`. + + +# Long-Running Workflows + +In this chapter, you’ll learn what a long-running workflow is and how to configure it. + +## What is a Long-Running Workflow? + +When you execute a workflow, you wait until the workflow finishes execution to receive the output. + +A long-running workflow is a workflow that continues its execution in the background. You don’t receive its output immediately. Instead, you subscribe to the workflow execution to listen to status changes and receive its result once the execution is finished. + +### Why use Long-Running Workflows? + +Long-running workflows are useful if: + +- A task takes too long. For example, you're importing data from a CSV file. +- The workflow's steps wait for an external action to finish before resuming execution. For example, before you import the data from the CSV file, you wait until the import is confirmed by the user. + +*** + +## Configure Long-Running Workflows + +A workflow is considered long-running if at least one step has its `async` configuration set to `true` and doesn't return a step response. + +For example, consider the following workflow and steps: + +```ts title="src/workflows/hello-world.ts" highlights={[["15"]]} collapsibleLines="1-11" expandButtonLabel="Show More" +import { + createStep, + createWorkflow, + WorkflowResponse, + StepResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep("step-1", async () => { + return new StepResponse({}) +}) + +const step2 = createStep( + { + name: "step-2", + async: true, + }, + async () => { + console.log("Waiting to be successful...") + } +) + +const step3 = createStep("step-3", async () => { + return new StepResponse("Finished three steps") +}) + +const myWorkflow = createWorkflow( + "hello-world", + function () { + step1() + step2() + const message = step3() + + return new WorkflowResponse({ + message, + }) +}) + +export default myWorkflow +``` + +The second step has in its configuration object `async` set to `true` and it doesn't return a step response. This indicates that this step is an asynchronous step. + +So, when you execute the `hello-world` workflow, it continues its execution in the background once it reaches the second step. + +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). + +*** + +## Change Step Status + +Once the workflow's execution reaches an async step, it'll wait in the background for the step to succeed or fail before it moves to the next step. + +To fail or succeed a step, use the Workflow Engine Module's main service that is registered in the Medusa Container under the `Modules.WORKFLOW_ENGINE` (or `workflowsModuleService`) key. + +### Retrieve Transaction ID + +Before changing the status of a workflow execution's async step, you must have the execution's transaction ID. + +When you execute the workflow, the object returned has a `transaction` property, which is an object that holds the details of the workflow execution's transaction. Use its `transactionId` to later change async steps' statuses: + +```ts +const { transaction } = await myWorkflow(req.scope) + .run() + +// use transaction.transactionId later +``` + +### Change Step Status to Successful + +The Workflow Engine Module's main service has a `setStepSuccess` method to set a step's status to successful. If you use it on a workflow execution's async step, the workflow continues execution to the next step. + +For example, consider the following step: + +```ts highlights={successStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + Modules, + TransactionHandlerType, +} from "@medusajs/framework/utils" +import { + StepResponse, + createStep, +} from "@medusajs/framework/workflows-sdk" + +type SetStepSuccessStepInput = { + transactionId: string +}; + +export const setStepSuccessStep = createStep( + "set-step-success-step", + async function ( + { transactionId }: SetStepSuccessStepInput, + { container } + ) { + const workflowEngineService = container.resolve( + Modules.WORKFLOW_ENGINE + ) + + await workflowEngineService.setStepSuccess({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId, + stepId: "step-2", + workflowId: "hello-world", + }, + stepResponse: new StepResponse("Done!"), + options: { + container, + }, + }) + } +) +``` + +In this step (which you use in a workflow other than the long-running workflow), you resolve the Workflow Engine Module's main service and set `step-2` of the previous workflow as successful. + +The `setStepSuccess` method of the workflow engine's main service accepts as a parameter an object having the following properties: + +- idempotencyKey: (\`object\`) The details of the workflow execution. + + - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. + + - 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. + + - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow. +- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the \`async\` step doesn't have a response, you set its response when changing its status. +- options: (\`Record\\`) Options to pass to the step. + + - container: (\`MedusaContainer\`) An instance of the Medusa Container + +### Change Step Status to Failed + +The Workflow Engine Module's main service also has a `setStepFailure` method that changes a step's status to failed. It accepts the same parameter as `setStepSuccess`. + +After changing the async step's status to failed, the workflow execution fails and the compensation functions of previous steps are executed. + +For example: + +```ts highlights={failureStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + Modules, + TransactionHandlerType, +} from "@medusajs/framework/utils" +import { + StepResponse, + createStep, +} from "@medusajs/framework/workflows-sdk" + +type SetStepFailureStepInput = { + transactionId: string +}; + +export const setStepFailureStep = createStep( + "set-step-success-step", + async function ( + { transactionId }: SetStepFailureStepInput, + { container } + ) { + const workflowEngineService = container.resolve( + Modules.WORKFLOW_ENGINE + ) + + await workflowEngineService.setStepFailure({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId, + stepId: "step-2", + workflowId: "hello-world", + }, + stepResponse: new StepResponse("Failed!"), + options: { + container, + }, + }) + } +) +``` + +You use this step in another workflow that changes the status of an async step in a long-running workflow's execution to failed. + +*** + +## Access Long-Running Workflow Status and Result + +To access the status and result of a long-running workflow execution, use the `subscribe` and `unsubscribe` methods of the Workflow Engine Module's main service. + +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/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-11" expandButtonLabel="Show Imports" +import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import myWorkflow from "../../../workflows/hello-world" +import { + IWorkflowEngineService, +} from "@medusajs/framework/types" +import { Modules } from "@medusajs/framework/utils" + +export async function GET(req: MedusaRequest, res: MedusaResponse) { + const { transaction, result } = await myWorkflow(req.scope).run() + + const workflowEngineService = req.scope.resolve< + IWorkflowEngineService + >( + Modules.WORKFLOW_ENGINE + ) + + const subscriptionOptions = { + workflowId: "hello-world", + transactionId: transaction.transactionId, + subscriberId: "hello-world-subscriber", + } + + await workflowEngineService.subscribe({ + ...subscriptionOptions, + subscriber: async (data) => { + if (data.eventType === "onFinish") { + console.log("Finished execution", data.result) + // unsubscribe + await workflowEngineService.unsubscribe({ + ...subscriptionOptions, + subscriberOrId: subscriptionOptions.subscriberId, + }) + } else if (data.eventType === "onStepFailure") { + console.log("Workflow failed", data.step) + } + }, + }) + + res.send(result) +} +``` + +In the above example, you execute the long-running workflow `hello-world` and resolve the Workflow Engine Module's main service from the Medusa container. + +### subscribe Method + +The main service's `subscribe` method allows you to listen to changes in the workflow execution’s status. It accepts an object having three properties: + +- workflowId: (\`string\`) The name of the workflow. +- transactionId: (\`string\`) The ID of the workflow exection's transaction. The transaction's details are returned in the response of the workflow execution. +- subscriberId: (\`string\`) The ID of the subscriber. +- subscriber: (\`(data: \{ eventType: string, result?: any }) => Promise\\`) The function executed when the workflow execution's status changes. The function receives a data object. It has an \`eventType\` property, which you use to check the status of the workflow execution. + +If the value of `eventType` in the `subscriber` function's first parameter is `onFinish`, the workflow finished executing. The first parameter then also has a `result` property holding the workflow's output. + +### unsubscribe Method + +You can unsubscribe from the workflow using the workflow engine's `unsubscribe` method, which requires the same object parameter as the `subscribe` method. + +However, instead of the `subscriber` property, it requires a `subscriberOrId` property whose value is the same `subscriberId` passed to the `subscribe` method. + +*** + +## Example: Restaurant-Delivery Recipe + +To find a full example of a long-running workflow, refer to the [restaurant-delivery recipe](https://docs.medusajs.com/resources/recipes/marketplace/examples/restaurant-delivery/index.html.md). + +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. @@ -14656,294 +12758,6 @@ const step1 = createStep( ``` -# Long-Running Workflows - -In this chapter, you’ll learn what a long-running workflow is and how to configure it. - -## What is a Long-Running Workflow? - -When you execute a workflow, you wait until the workflow finishes execution to receive the output. - -A long-running workflow is a workflow that continues its execution in the background. You don’t receive its output immediately. Instead, you subscribe to the workflow execution to listen to status changes and receive its result once the execution is finished. - -### Why use Long-Running Workflows? - -Long-running workflows are useful if: - -- A task takes too long. For example, you're importing data from a CSV file. -- The workflow's steps wait for an external action to finish before resuming execution. For example, before you import the data from the CSV file, you wait until the import is confirmed by the user. - -*** - -## Configure Long-Running Workflows - -A workflow is considered long-running if at least one step has its `async` configuration set to `true` and doesn't return a step response. - -For example, consider the following workflow and steps: - -```ts title="src/workflows/hello-world.ts" highlights={[["15"]]} collapsibleLines="1-11" expandButtonLabel="Show More" -import { - createStep, - createWorkflow, - WorkflowResponse, - StepResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep("step-1", async () => { - return new StepResponse({}) -}) - -const step2 = createStep( - { - name: "step-2", - async: true, - }, - async () => { - console.log("Waiting to be successful...") - } -) - -const step3 = createStep("step-3", async () => { - return new StepResponse("Finished three steps") -}) - -const myWorkflow = createWorkflow( - "hello-world", - function () { - step1() - step2() - const message = step3() - - return new WorkflowResponse({ - message, - }) -}) - -export default myWorkflow -``` - -The second step has in its configuration object `async` set to `true` and it doesn't return a step response. This indicates that this step is an asynchronous step. - -So, when you execute the `hello-world` workflow, it continues its execution in the background once it reaches the second step. - -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). - -*** - -## Change Step Status - -Once the workflow's execution reaches an async step, it'll wait in the background for the step to succeed or fail before it moves to the next step. - -To fail or succeed a step, use the Workflow Engine Module's main service that is registered in the Medusa Container under the `Modules.WORKFLOW_ENGINE` (or `workflowsModuleService`) key. - -### Retrieve Transaction ID - -Before changing the status of a workflow execution's async step, you must have the execution's transaction ID. - -When you execute the workflow, the object returned has a `transaction` property, which is an object that holds the details of the workflow execution's transaction. Use its `transactionId` to later change async steps' statuses: - -```ts -const { transaction } = await myWorkflow(req.scope) - .run() - -// use transaction.transactionId later -``` - -### Change Step Status to Successful - -The Workflow Engine Module's main service has a `setStepSuccess` method to set a step's status to successful. If you use it on a workflow execution's async step, the workflow continues execution to the next step. - -For example, consider the following step: - -```ts highlights={successStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - Modules, - TransactionHandlerType, -} from "@medusajs/framework/utils" -import { - StepResponse, - createStep, -} from "@medusajs/framework/workflows-sdk" - -type SetStepSuccessStepInput = { - transactionId: string -}; - -export const setStepSuccessStep = createStep( - "set-step-success-step", - async function ( - { transactionId }: SetStepSuccessStepInput, - { container } - ) { - const workflowEngineService = container.resolve( - Modules.WORKFLOW_ENGINE - ) - - await workflowEngineService.setStepSuccess({ - idempotencyKey: { - action: TransactionHandlerType.INVOKE, - transactionId, - stepId: "step-2", - workflowId: "hello-world", - }, - stepResponse: new StepResponse("Done!"), - options: { - container, - }, - }) - } -) -``` - -In this step (which you use in a workflow other than the long-running workflow), you resolve the Workflow Engine Module's main service and set `step-2` of the previous workflow as successful. - -The `setStepSuccess` method of the workflow engine's main service accepts as a parameter an object having the following properties: - -- idempotencyKey: (\`object\`) The details of the workflow execution. - - - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. - - - 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. - - - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow. -- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the \`async\` step doesn't have a response, you set its response when changing its status. -- options: (\`Record\\`) Options to pass to the step. - - - container: (\`MedusaContainer\`) An instance of the Medusa Container - -### Change Step Status to Failed - -The Workflow Engine Module's main service also has a `setStepFailure` method that changes a step's status to failed. It accepts the same parameter as `setStepSuccess`. - -After changing the async step's status to failed, the workflow execution fails and the compensation functions of previous steps are executed. - -For example: - -```ts highlights={failureStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - Modules, - TransactionHandlerType, -} from "@medusajs/framework/utils" -import { - StepResponse, - createStep, -} from "@medusajs/framework/workflows-sdk" - -type SetStepFailureStepInput = { - transactionId: string -}; - -export const setStepFailureStep = createStep( - "set-step-success-step", - async function ( - { transactionId }: SetStepFailureStepInput, - { container } - ) { - const workflowEngineService = container.resolve( - Modules.WORKFLOW_ENGINE - ) - - await workflowEngineService.setStepFailure({ - idempotencyKey: { - action: TransactionHandlerType.INVOKE, - transactionId, - stepId: "step-2", - workflowId: "hello-world", - }, - stepResponse: new StepResponse("Failed!"), - options: { - container, - }, - }) - } -) -``` - -You use this step in another workflow that changes the status of an async step in a long-running workflow's execution to failed. - -*** - -## Access Long-Running Workflow Status and Result - -To access the status and result of a long-running workflow execution, use the `subscribe` and `unsubscribe` methods of the Workflow Engine Module's main service. - -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/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-11" expandButtonLabel="Show Imports" -import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import myWorkflow from "../../../workflows/hello-world" -import { - IWorkflowEngineService, -} from "@medusajs/framework/types" -import { Modules } from "@medusajs/framework/utils" - -export async function GET(req: MedusaRequest, res: MedusaResponse) { - const { transaction, result } = await myWorkflow(req.scope).run() - - const workflowEngineService = req.scope.resolve< - IWorkflowEngineService - >( - Modules.WORKFLOW_ENGINE - ) - - const subscriptionOptions = { - workflowId: "hello-world", - transactionId: transaction.transactionId, - subscriberId: "hello-world-subscriber", - } - - await workflowEngineService.subscribe({ - ...subscriptionOptions, - subscriber: async (data) => { - if (data.eventType === "onFinish") { - console.log("Finished execution", data.result) - // unsubscribe - await workflowEngineService.unsubscribe({ - ...subscriptionOptions, - subscriberOrId: subscriptionOptions.subscriberId, - }) - } else if (data.eventType === "onStepFailure") { - console.log("Workflow failed", data.step) - } - }, - }) - - res.send(result) -} -``` - -In the above example, you execute the long-running workflow `hello-world` and resolve the Workflow Engine Module's main service from the Medusa container. - -### subscribe Method - -The main service's `subscribe` method allows you to listen to changes in the workflow execution’s status. It accepts an object having three properties: - -- workflowId: (\`string\`) The name of the workflow. -- transactionId: (\`string\`) The ID of the workflow exection's transaction. The transaction's details are returned in the response of the workflow execution. -- subscriberId: (\`string\`) The ID of the subscriber. -- subscriber: (\`(data: \{ eventType: string, result?: any }) => Promise\\`) The function executed when the workflow execution's status changes. The function receives a data object. It has an \`eventType\` property, which you use to check the status of the workflow execution. - -If the value of `eventType` in the `subscriber` function's first parameter is `onFinish`, the workflow finished executing. The first parameter then also has a `result` property holding the workflow's output. - -### unsubscribe Method - -You can unsubscribe from the workflow using the workflow engine's `unsubscribe` method, which requires the same object parameter as the `subscribe` method. - -However, instead of the `subscriber` property, it requires a `subscriberOrId` property whose value is the same `subscriberId` passed to the `subscribe` method. - -*** - -## Example: Restaurant-Delivery Recipe - -To find a full example of a long-running workflow, refer to the [restaurant-delivery recipe](https://docs.medusajs.com/resources/recipes/marketplace/examples/restaurant-delivery/index.html.md). - -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. - - # Multiple Step Usage in Workflow In this chapter, you'll learn how to use a step multiple times in a workflow. @@ -15072,92 +12886,6 @@ It returns an array of the steps' results in the same order they're passed to th 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. - -## Configure a Step’s Retrial - -By default, when an error occurs in a step, the step and the workflow fail, and the execution stops. - -You can configure the step to retry on failure. The `createStep` function can accept a configuration object instead of the step’s name as a first parameter. - -For example: - -```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - createStep, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - { - name: "step-1", - maxRetries: 2, - }, - async () => { - console.log("Executing step 1") - - throw new Error("Oops! Something happened.") - } -) - -const myWorkflow = createWorkflow( - "hello-world", - function () { - const str1 = step1() - - return new WorkflowResponse({ - message: str1, - }) -}) - -export default myWorkflow -``` - -The step’s configuration object accepts a `maxRetries` property, which is a number indicating the number of times a step can be retried when it fails. - -When you execute the above workflow, you’ll see the following result in the terminal: - -```bash -Executing step 1 -Executing step 1 -Executing step 1 -error: Oops! Something happened. -Error: Oops! Something happened. -``` - -The first line indicates the first time the step was executed, and the next two lines indicate the times the step was retried. After that, the step and workflow fail. - -*** - -## Step Retry Intervals - -By default, a step is retried immediately after it fails. To specify a wait time before a step is retried, pass a `retryInterval` property to the step's configuration object. Its value is a number of seconds to wait before retrying the step. - -For example: - -```ts title="src/workflows/hello-world.ts" highlights={[["5"]]} -const step1 = createStep( - { - name: "step-1", - maxRetries: 2, - retryInterval: 2, // 2 seconds - }, - async () => { - // ... - } -) -``` - -### Interval Changes Workflow to Long-Running - -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. - -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). - - # Store Workflow Executions In this chapter, you'll learn how to store workflow executions in the database and access them later. @@ -15303,6 +13031,669 @@ if (workflowExecution.state === "failed") { Other state values include `done`, `invoking`, and `compensating`. +# Retry Failed Steps + +In this chapter, you’ll learn how to configure steps to allow retrial on failure. + +## Configure a Step’s Retrial + +By default, when an error occurs in a step, the step and the workflow fail, and the execution stops. + +You can configure the step to retry on failure. The `createStep` function can accept a configuration object instead of the step’s name as a first parameter. + +For example: + +```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + { + name: "step-1", + maxRetries: 2, + }, + async () => { + console.log("Executing step 1") + + throw new Error("Oops! Something happened.") + } +) + +const myWorkflow = createWorkflow( + "hello-world", + function () { + const str1 = step1() + + return new WorkflowResponse({ + message: str1, + }) +}) + +export default myWorkflow +``` + +The step’s configuration object accepts a `maxRetries` property, which is a number indicating the number of times a step can be retried when it fails. + +When you execute the above workflow, you’ll see the following result in the terminal: + +```bash +Executing step 1 +Executing step 1 +Executing step 1 +error: Oops! Something happened. +Error: Oops! Something happened. +``` + +The first line indicates the first time the step was executed, and the next two lines indicate the times the step was retried. After that, the step and workflow fail. + +*** + +## Step Retry Intervals + +By default, a step is retried immediately after it fails. To specify a wait time before a step is retried, pass a `retryInterval` property to the step's configuration object. Its value is a number of seconds to wait before retrying the step. + +For example: + +```ts title="src/workflows/hello-world.ts" highlights={[["5"]]} +const step1 = createStep( + { + name: "step-1", + maxRetries: 2, + retryInterval: 2, // 2 seconds + }, + async () => { + // ... + } +) +``` + +### Interval Changes Workflow to Long-Running + +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. + +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). + + +# 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. + +## Why Variable Manipulation isn't Allowed in Workflows + +Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. + +At that point, variables in the workflow don't have any values. They only do when you execute the workflow. + +So, you can only pass variables as parameters to steps. But, in a workflow, you can't change a variable's value or, if the variable is an array, loop over its items. + +Instead, use `transform` from the Workflows SDK. + +Restrictions for variable manipulation is only applicable in a workflow's definition. You can still manipulate variables in your step's code. + +*** + +## What is the transform Utility? + +`transform` creates a new variable as the result of manipulating other variables. + +For example, consider you have two strings as the output of two steps: + +```ts +const str1 = step1() +const str2 = step2() +``` + +To concatenate the strings, you create a new variable `str3` using the `transform` function: + +```ts highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + transform, +} from "@medusajs/framework/workflows-sdk" +// step imports... + +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str1 = step1(input) + const str2 = step2(input) + + const str3 = transform( + { str1, str2 }, + (data) => `${data.str1}${data.str2}` + ) + + return new WorkflowResponse(str3) + } +) +``` + +`transform` accepts two parameters: + +1. The first parameter is an object of variables to manipulate. The object is passed as a parameter to `transform`'s second parameter function. +2. The second parameter is the function performing the variable manipulation. + +The value returned by the second parameter function is returned by `transform`. So, the `str3` variable holds the concatenated string. + +You can use the returned value in the rest of the workflow, either to pass it as an input to other steps or to return it in the workflow's response. + +*** + +## Example: Looping Over Array + +Use `transform` to loop over arrays to create another variable from the array's items. + +For example: + +```ts collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createWorkflow, + WorkflowResponse, + transform, +} from "@medusajs/framework/workflows-sdk" +// step imports... + +type WorkflowInput = { + items: { + id: string + name: string + }[] +} + +const myWorkflow = createWorkflow( + "hello-world", + function ({ items }: WorkflowInput) { + const ids = transform( + { items }, + (data) => data.items.map((item) => item.id) + ) + + doSomethingStep(ids) + + // ... + } +) +``` + +This workflow receives an `items` array in its input. + +You use `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`. + +*** + +## Example: Creating a Date + +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: + +```ts +const myWorkflow = createWorkflow( + "hello-world", + () => { + const today = transform({}, () => new Date()) + + doSomethingStep(today) + } +) +``` + +In this workflow, `today` is only evaluated when the workflow is executed. + +*** + +## 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") + } +) +``` + +Since `str`'s value isn't used as a step's input or passed to `WorkflowResponse`, its value is never evaluated. + +### Data Validation + +`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. + +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. + } +) +``` + + +# 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. + +### Prerequisites + +- [Brands Module](https://docs.medusajs.com/learn/customization/custom-features/modules/index.html.md) + +## 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. + +Replace or create the `GET` API route at `src/api/admin/brands/route.ts` with the following: + +```ts title="src/api/admin/brands/route.ts" highlights={apiRouteHighlights} +// 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, + metadata: { count, take, skip } = {}, + } = await query.graph({ + entity: "brand", + ...req.queryConfig, + }) + + res.json({ + brands, + count, + limit: take, + offset: skip, + }) +} +``` + +In the API route, you use Query's `graph` method to retrieve the brands. In the method's object parameter, you spread the `queryConfig` property of the request object. This property holds configurations for pagination and retrieved fields. + +The query configurations are combined from default configurations, which you'll add next, and the request's query parameters: + +- `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. + +When you pass pagination configurations to the `graph` method, the returned object has the pagination's details in a `metadata` property, whose value is an object having the following properties: + +- `count`: The total count of items. +- `take`: The maximum number of items returned in the `data` array. +- `skip`: The number of items skipped before retrieving the returned items. + +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). + +*** + +## 2. Add Default Query Configurations + +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: + +```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() + +export default defineMiddlewares({ + routes: [ + // ... + { + matcher: "/admin/brands", + method: "GET", + middlewares: [ + validateAndTransformQuery( + GetBrandsSchema, + { + defaults: [ + "id", + "name", + "products.*", + ], + isList: true, + } + ), + ], + }, + + ], +}) +``` + +You apply the `validateAndTransformQuery` middleware on the `GET /admin/brands` API route. The middleware accepts two parameters: + +- A [Zod](https://zod.dev/) schema that a request's query parameters must satisfy. Medusa provides `createFindParams` that generates a Zod schema with the following properties: + - `fields`: A comma-separated string indicating the fields to retrieve. + - `limit`: The maximum number of items to retrieve. + - `offset`: The number of items to skip before retrieving the returned items. + - `order`: The name of the field to sort the items by. Learn more about sorting in [the API reference](https://docs.medusajs.com/api/admin#sort-order) +- An object of Query configurations having the following properties: + - `defaults`: An array of default fields and relations to retrieve. + - `isList`: Whether the API route returns a list of items. + +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. + +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). + +*** + +## 3. Initialize JS SDK + +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: + +![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg) + +```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). + +*** + +## 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: + +![Directory structure of the Medusa application after adding the UI route.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733472011/Medusa%20Book/brands-admin-dir-overview-3_syytld.jpg) + +```tsx title="src/admin/routes/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 + + return ( + + {/* TODO show brands */} + + ) +} + +export const config = defineRouteConfig({ + label: "Brands", + icon: TagSolid, +}) + +export default BrandsPage +``` + +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. + +First, add the following type in `src/admin/routes/brands/page.tsx`: + +```tsx title="src/admin/routes/brands/page.tsx" +type Brand = { + id: string + name: string +} +type BrandsResponse = { + brands: Brand[] + count: number + limit: number + offset: number +} +``` + +You define the type for a brand, and the type of expected response from the `GET /admin/brands` API route. + +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`: + +```tsx title="src/admin/routes/brands/page.tsx" +import { + // ... + Heading, + createDataTableColumnHelper, + DataTable, + DataTablePaginationState, + useDataTable, +} from "@medusajs/ui" +``` + +You import the `DataTable` component and the following utilities: + +- `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. + +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: + +```tsx title="src/admin/routes/brands/page.tsx" +const columnHelper = createDataTableColumnHelper() + +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. + +Then, replace the `// TODO retrieve brands` in the component with the following: + +```tsx title="src/admin/routes/brands/page.tsx" highlights={queryHighlights} +const limit = 15 +const [pagination, setPagination] = useState({ + pageSize: limit, + pageIndex: 0, +}) +const offset = useMemo(() => { + return pagination.pageIndex * limit +}, [pagination]) + +const { data, isLoading } = useQuery({ + queryFn: () => sdk.client.fetch(`/admin/brands`, { + query: { + limit, + offset, + }, + }), + queryKey: [["brands", limit, offset]], +}) + +// TODO configure data table +``` + +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: + +- `pageSize`: The maximum number of items per page. You set it to `15`. +- `pageIndex`: A zero-based index of the current page of items. + +You also define a memoized `offset` value that indicates the number of items to skip before retrieving the current page's items. + +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. + +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. + +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. + +### Display Brands Table + +Finally, you'll display the brands in a data table. Replace the `// TODO configure data table` in the component with the following: + +```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, + }, +}) +``` + +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. + +Then, replace the `{/* TODO show brands */}` in the return statement with the following: + +```tsx title="src/admin/routes/brands/page.tsx" + + + Brands + + + + +``` + +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. + +*** + +## Test it Out + +To test out the UI route, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, you'll find a new "Brands" sidebar item. Click on it to see the brands in your store. You can also go to `http://localhost:9000/app/brands` to see the page. + +![A new sidebar item is added for the new brands UI route. The UI route shows the table of brands with pagination.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733421074/Medusa%20Book/Screenshot_2024-12-05_at_7.46.52_PM_slcdqd.png) + +*** + +## Summary + +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. + +*** + +## Next Steps: Integrate Third-Party Systems + +Your customizations often span across systems, where you need to retrieve data or perform operations in a third-party system. + +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. + + # Workflow Timeout In this chapter, you’ll learn how to set a timeout for workflows and steps. @@ -15513,209 +13904,1818 @@ export async function POST(req: MedusaRequest, res: MedusaResponse) { Your hook handler then receives that passed data in the `additional_data` object. -# Variable Manipulation in Workflows with transform +# Guide: Add Product's Brand Widget in Admin -In this chapter, you'll learn how to use `transform` from the Workflows SDK to manipulate variables in a workflow. +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. -## Why Variable Manipulation isn't Allowed in Workflows +### Prerequisites -Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. +- [Brands linked to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) -At that point, variables in the workflow don't have any values. They only do when you execute the workflow. +## 1. Initialize JS SDK -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. +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. -Instead, use `transform` from the Workflows SDK. +So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content: -Restrictions for variable manipulation is only applicable in a workflow's definition. You can still manipulate variables in your step's code. +![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg) + +```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). *** -## What is the transform Utility? +## 2. Add Widget to Product Details Page -`transform` creates a new variable as the result of manipulating other variables. +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. -For example, consider you have two strings as the output of two steps: +Learn more about widgets in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md). -```ts -const str1 = step1() -const str2 = step2() +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: + +![Directory structure of the Medusa application after adding the widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414684/Medusa%20Book/brands-admin-dir-overview-2_eq5xhi.jpg) + +```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) => { + const { data: queryResult } = useQuery({ + queryFn: () => sdk.admin.product.retrieve(product.id, { + fields: "+brand.*", + }), + queryKey: [["product", product.id]], + }) + const brandName = (queryResult?.product as AdminProductBrand)?.brand?.name + + return ( + +
+
+ Brand +
+
+
+ + Name + + + + {brandName || "-"} + +
+
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductBrandWidget ``` -To concatenate the strings, you create a new variable `str3` using the `transform` function: +A widget's file must export: -```ts highlights={highlights} +- 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. + +![The widget is added as the first section of the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414415/Medusa%20Book/Screenshot_2024-12-05_at_5.59.25_PM_y85m14.png) + +*** + +## 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: + +![The directory structure of the Medusa application after adding the link.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733329897/Medusa%20Book/brands-link-dir-overview_t1rhlp.jpg) + +```ts title="src/links/product-brand.ts" highlights={highlights} +import BrandModule from "../modules/brand" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + { + linkable: ProductModule.linkable.product, + isList: true, + }, + BrandModule.linkable.brand +) +``` + +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: Query Product's Brands + +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 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](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. + +Learn more about selecting fields and relations in the [API Reference](https://docs.medusajs.com/api/admin#select-fields-and-relations). + +For example, send the following request to retrieve the list of products with their brands: + +```bash +curl 'http://localhost:9000/admin/products?fields=+brand.*' \ +--header 'Authorization: Bearer {token}' +``` + +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: + +```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: + +![Directory structure after creating the hook's file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733384338/Medusa%20Book/brands-hook-dir-overview_ltwr5h.jpg) + +```ts title="src/workflows/hooks/created-product.ts" highlights={hook1Highlights} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +import { StepResponse } from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" +import { LinkDefinition } from "@medusajs/framework/types" +import { BRAND_MODULE } from "../../modules/brand" +import BrandModuleService from "../../modules/brand/service" + +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`. + +![Diagram showcasing how the order of defining a link affects creating the link](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386156/Medusa%20Book/remote-link-brand-product-exp_fhjmg4.jpg) + +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: + +![Directory structure after adding the middelwares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386868/Medusa%20Book/brands-middleware-dir-overview_uczos1.jpg) + +```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. + + +# 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. + +An API Route is an endpoint that acts as an entry point for other clients to interact with your Medusa customizations, such as the admin dashboard, storefronts, or third-party systems. + +The 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. + +### Prerequisites + +- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) + +## 1. Create the API Route + +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…). + +Learn more about API routes [in this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/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: + +![Directory structure of the Medusa application after adding the route](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869882/Medusa%20Book/brand-route-dir-overview-2_hjqlnf.jpg) + +```ts title="src/api/admin/brands/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" import { - createWorkflow, - WorkflowResponse, - transform, + createBrandWorkflow, +} from "../../../workflows/create-brand" + +type PostAdminCreateBrandType = { + name: string +} + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { result } = await createBrandWorkflow(req.scope) + .run({ + input: req.validatedBody, + }) + + res.json({ brand: result }) +} +``` + +You export a route handler function with its name (`POST`) being the HTTP method of the API route you're exposing. + +The 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. + +`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`. + +You return a JSON response with the created brand using the `res.json` method. + +*** + +## 2. Create Validation Schema + +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. + +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). + +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: + +![Directory structure of Medusa application after adding validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869806/Medusa%20Book/brand-route-dir-overview-1_yfyjss.jpg) + +```ts title="src/api/admin/brands/validators.ts" +import { z } from "zod" + +export const PostAdminCreateBrand = z.object({ + name: z.string(), +}) +``` + +You export a validation schema that expects in the request body an object having a `name` property whose value is a string. + +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" + +type PostAdminCreateBrandType = z.infer + +// ... +``` + +*** + +## 3. Add Validation Middleware + +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. + +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. + +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: + +![Directory structure of the Medusa application after adding the middleware](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869977/Medusa%20Book/brand-route-dir-overview-3_kcx511.jpg) + +```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 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. + +In the middleware object, you define three properties: + +- `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. + +*** + +## Test API Route + +To test out the API route, start the Medusa application with the following command: + +```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 returns the created brand in the response: + +```json title="Example Response" +{ + "brand": { + "id": "01J7AX9ES4X113HKY6C681KDZJ", + "name": "Acme", + "created_at": "2024-09-09T08:09:34.244Z", + "updated_at": "2024-09-09T08:09:34.244Z" + } +} +``` + +*** + +## Summary + +By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand. You did that by: + +1. Creating a module that defines and manages a `brand` table in the database. +2. Creating a workflow that uses the module's service to create a brand record, and implements the compensation logic to delete that brand in case an error occurs. +3. Creating an API route that allows admin users to create a brand. + +*** + +## 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 next chapters, you'll learn how to build associations between data models defined in different modules. + + +# 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: + +![Directory structure in the Medusa project after adding the file for createBrandStep](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869184/Medusa%20Book/brand-workflow-dir-overview-1_fjvf5j.jpg) + +```ts title="src/workflows/create-brand.ts" +import { + createStep, + StepResponse, } from "@medusajs/framework/workflows-sdk" -// step imports... +import { BRAND_MODULE } from "../modules/brand" +import BrandModuleService from "../modules/brand/service" -const myWorkflow = createWorkflow( - "hello-world", - function (input) { - const str1 = step1(input) - const str2 = step2(input) +export type CreateBrandStepInput = { + name: string +} - const str3 = transform( - { str1, str2 }, - (data) => `${data.str1}${data.str2}` +export const createBrandStep = createStep( + "create-brand-step", + async (input: CreateBrandStepInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE ) - return new WorkflowResponse(str3) + 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: 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: + +![Directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493547/Medusa%20Book/cms-dir-overview-4_u5t0ug.jpg) + +```ts title="src/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 +} + +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: + +![Directory structure of the Medusa application after adding the subscriber](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493774/Medusa%20Book/cms-dir-overview-5_iqqwvg.jpg) + +```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: 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. + +![Directory structure in Medusa project after adding the brand directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868844/Medusa%20Book/brand-dir-overview-1_hxwvgx.jpg) + +*** + +## 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: + +![Directory structure in module after adding the brand data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868920/Medusa%20Book/brand-dir-overview-2_lexhdl.jpg) + +```ts title="src/modules/brand/models/brand.ts" +import { model } from "@medusajs/framework/utils" + +export const Brand = model.define("brand", { + id: model.id().primaryKey(), + name: model.text(), +}) +``` + +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: + +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 about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties/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). + +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: + +![Directory structure in module after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868984/Medusa%20Book/brand-dir-overview-3_jo7baj.jpg) + +```ts title="src/modules/brand/service.ts" highlights={serviceHighlights} +import { MedusaService } from "@medusajs/framework/utils" +import { Brand } from "./models/brand" + +class BrandModuleService extends MedusaService({ + Brand, +}) { + +} + +export default BrandModuleService +``` + +The `BrandModuleService` extends a 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: + +![Directory structure in module after adding the definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869045/Medusa%20Book/brand-dir-overview-4_nf8ymw.jpg) + +```ts title="src/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). + +[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: + +```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. + +*** + +## 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. + + +# 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: + +![Directory structure of the Medusa application after creating the file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494196/Medusa%20Book/cms-dir-overview-6_z1omsi.jpg) + +```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), + }) + + const updatedBrands = await brandModuleService.updateBrands(brands) + + return new StepResponse(updatedBrands, prevUpdatedBrands) + }, + async (prevUpdatedBrands, { container }) => { + if (!prevUpdatedBrands) { + return + } + + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + await brandModuleService.updateBrands(prevUpdatedBrands) + } +) +``` + +The `updateBrandsStep` receives the brands to update in Medusa. In the step, you retrieve the brand's details in Medusa before the update to pass them to the compensation function. You then update the brands using the Brand Module's `updateBrands` generated method. + +In the compensation function, which receives the brand's old data, you revert the update using the same `updateBrands` method. + +### Create Workflow + +Finally, you'll create the workflow that uses the above steps to sync the brands from the CMS to Medusa. Add to the same `src/workflows/sync-brands-from-cms.ts` file the following: + +```ts title="src/workflows/sync-brands-from-cms.ts" +// other imports... +import { + // ... + createWorkflow, + transform, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +// ... + +export const syncBrandsFromCmsWorkflow = createWorkflow( + "sync-brands-from-system", + () => { + const brands = retrieveBrandsFromCmsStep() + + // TODO create and update brands + } +) +``` + +In the workflow, you only use the `retrieveBrandsFromCmsStep` for now, which retrieves the brands from the third-party CMS. + +Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use [transform](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values. + +Learn more about data manipulation using `transform` in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). + +So, replace the `TODO` with the following: + +```ts title="src/workflows/sync-brands-from-cms.ts" +const { toCreate, toUpdate } = transform( + { + brands, + }, + (data) => { + const toCreate: CreateBrand[] = [] + const toUpdate: UpdateBrand[] = [] + + data.brands.forEach((brand) => { + if (brand.external_id) { + toUpdate.push({ + id: brand.external_id as string, + name: brand.name as string, + }) + } else { + toCreate.push({ + name: brand.name as string, + }) + } + }) + + return { toCreate, toUpdate } + } +) + +// TODO create and update the brands +``` + `transform` accepts two parameters: -1. The first parameter is an object of variables to manipulate. The object is passed as a parameter to `transform`'s second parameter function. -2. The second parameter is the function performing the variable manipulation. +1. The data to be passed to the function in the second parameter. +2. A function to execute only when the workflow is executed. Its return value can be consumed by the rest of the workflow. -The value returned by the second parameter function is returned by `transform`. So, the `str3` variable holds the concatenated string. +In `transform`'s function, you loop over the brands array to check which should be created or updated. This logic assumes that a brand in the CMS has an `external_id` property whose value is the brand's ID in Medusa. -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. +You now have the list of brands to create and update. So, replace the new `TODO` with the following: + +```ts title="src/workflows/sync-brands-from-cms.ts" +const created = createBrandsStep({ brands: toCreate }) +const updated = updateBrandsStep({ brands: toUpdate }) + +return new WorkflowResponse({ + created, + updated, +}) +``` + +You first run the `createBrandsStep` to create the brands that don't exist in Medusa, then the `updateBrandsStep` to update the brands that exist in Medusa. You pass the arrays returned by `transform` as the inputs for the steps. + +Finally, you return an object of the created and updated brands. You'll execute this workflow in the scheduled job next. *** -## Example: Looping Over Array +## 2. Schedule Syncing Task -Use `transform` to loop over arrays to create another variable from the array's items. +You now have the workflow to sync the brands from the CMS to Medusa. Next, you'll create a scheduled job that runs this workflow once a day to ensure the data between Medusa and the CMS are always in sync. -For example: +A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. So, create the file `src/jobs/sync-brands-from-cms.ts` with the following content: -```ts collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - createWorkflow, - WorkflowResponse, - transform, -} from "@medusajs/framework/workflows-sdk" -// step imports... +![Directory structure of the Medusa application after adding the scheduled job](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494592/Medusa%20Book/cms-dir-overview-7_dkjb9s.jpg) -type WorkflowInput = { - items: { - id: string - name: string - }[] +```ts title="src/jobs/sync-brands-from-cms.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { syncBrandsFromCmsWorkflow } from "../workflows/sync-brands-from-cms" + +export default async function (container: MedusaContainer) { + const logger = container.resolve("logger") + + const { result } = await syncBrandsFromCmsWorkflow(container).run() + + logger.info( + `Synced brands from third-party system: ${ + result.created.length + } brands created and ${result.updated.length} brands updated.`) } -const myWorkflow = createWorkflow( - "hello-world", - function ({ items }: WorkflowInput) { - const ids = transform( - { items }, - (data) => data.items.map((item) => item.id) - ) - - doSomethingStep(ids) +export const config = { + name: "sync-brands-from-system", + schedule: "0 0 * * *", // change to * * * * * for debugging +} +``` +A scheduled job file must export: + +- An asynchronous function that will be executed at the specified schedule. This function must be the file's default export. +- An object of scheduled jobs configuration. It has two properties: + - `name`: A unique name for the scheduled job. + - `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. + +The scheduled job function accepts as a parameter the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) used to resolve framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated. + +Based on the cron expression specified in `config.schedule`, Medusa will run the scheduled job every day at midnight. You can also change it to `* * * * *` to run it every minute for easier debugging. + +*** + +## Test it Out + +To test out the scheduled job, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +If you set the schedule to `* * * * *` for debugging, the scheduled job will run in a minute. You'll see in the logs how many brands were created or updated. + +*** + +## Summary + +By following the previous chapters, you utilized Medusa's framework and orchestration tools to perform and automate tasks that span across systems. + +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. + +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. + +![Directory structure after adding the directory for the CMS Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492447/Medusa%20Book/cms-dir-overview-1_gasguk.jpg) + +*** + +## 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: + +![Directory structure after adding the CMS Module's service](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492583/Medusa%20Book/cms-dir-overview-2_zwcwh3.jpg) + +```ts title="src/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 + this.options_ = options + + // TODO initialize SDK + } +} + +export default CmsModuleService +``` + +You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters: + +1. The module's container. Since a module is [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), it has a [local container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) different than the Medusa container you use in other customizations. This container holds framework tools like the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) and resources within the module. +2. Options passed to the module when it's later added in Medusa's configurations. These options are useful to pass secret keys or configurations that ensure your module is re-usable across applications. For the CMS Module, you accept the API key to connect to the dummy CMS as an option. + +When integrating a third-party system that has a Node.js SDK or client, you can initialize that client in the constructor to be used in the service's methods. + +### Integration Methods + +Next, you'll add methods that simulate sending requests to a third-party CMS. You'll use these methods later to sync brands from and to the CMS. + +Add the following methods in the `CmsModuleService`: + +```ts title="src/modules/cms/service.ts" highlights={methodsHighlights} +export class CmsModuleService { + // ... + + // a dummy method to simulate sending a request, + // in a realistic scenario, you'd use an SDK, fetch, or axios clients + private async sendRequest(url: string, method: string, data?: any) { + this.logger_.info(`Sending a ${method} request to ${url}.`) + this.logger_.info(`Request Data: ${JSON.stringify(data, null, 2)}`) + this.logger_.info(`API Key: ${JSON.stringify(this.options_.apiKey, null, 2)}`) + } + + async createBrand(brand: Record) { + await this.sendRequest("/brands", "POST", brand) + } + + async deleteBrand(id: string) { + await this.sendRequest(`/brands/${id}`, "DELETE") + } + + async retrieveBrands(): Promise[]> { + await this.sendRequest("/brands", "GET") + + return [] + } +} +``` + +The `sendRequest` method sends requests to the third-party CMS. Since this guide isn't using a real CMS, it only simulates the sending by logging messages in the terminal. + +You also add three methods that use the `sendRequest` method: + +- `createBrand` that creates a brand in the third-party system. +- `deleteBrand` that deletes the brand in the third-party system. +- `retrieveBrands` to retrieve a brand from the third-party system. + +*** + +## 3. Export Module Definition + +After creating the module's service, you'll export the module definition indicating the module's name and service. + +Create the file `src/modules/cms/index.ts` with the following content: + +![Directory structure of the Medusa application after adding the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492991/Medusa%20Book/cms-dir-overview-3_b0byks.jpg) + +```ts title="src/modules/cms/index.ts" +import { Module } from "@medusajs/framework/utils" +import CmsModuleService from "./service" + +export const CMS_MODULE = "cms" + +export default Module(CMS_MODULE, { + service: CmsModuleService, +}) +``` + +You use `Module` from the Modules SDK to export the module's defintion, indicating that the module's name is `cms` and its service is `CmsModuleService`. + +*** + +## 4. Add Module to Medusa's Configurations + +Finally, add the module to the Medusa configurations at `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ // ... - } -) + { + resolve: "./src/modules/cms", + options: { + apiKey: process.env.CMS_API_KEY, + }, + }, + ], +}) ``` -This workflow receives an `items` array in its input. +The object passed in `modules` accept an `options` property, whose value is an object of options to pass to the module. These are the options you receive in the `CmsModuleService`'s constructor. -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 can add the `CMS_API_KEY` environment variable to `.env`: -You then pass the `ids` variable as a parameter to the `doSomethingStep`. +```bash +CMS_API_KEY=123 +``` *** -## Example: Creating a Date +## Next Steps: Sync Brand From Medusa to CMS -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. +You can now use the CMS Module's service to perform actions on the third-party CMS. -So, use `transform` instead to create a date variable with `new Date()`. - -For example: - -```ts -const myWorkflow = createWorkflow( - "hello-world", - () => { - const today = transform({}, () => new Date()) - - doSomethingStep(today) - } -) -``` - -In this workflow, `today` is only evaluated when the workflow is executed. - -*** - -## 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") - } -) -``` - -Since `str`'s value isn't used as a step's input or passed to `WorkflowResponse`, its value is never evaluated. - -### Data Validation - -`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. - -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. - } -) -``` +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. # Docs Contribution Guidelines @@ -15971,135 +15971,98 @@ console.log("This block can't use semi colons") ~~~ */} -# Example: Write Integration Tests for Workflows +# Translate Medusa Admin -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. +The Medusa Admin supports multiple languages, with the default being English. In this documentation, you'll learn how to contribute to the community by translating the Medusa Admin to a language you're fluent in. -### Prerequisites +{/* vale docs.We = NO */} -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) +You can contribute either by translating the admin to a new language, or fixing translations for existing languages. As we can't validate every language's translations, some translations may be incorrect. Your contribution is welcome to fix any translation errors you find. -## Write Integration Test for Workflow +{/* vale docs.We = YES */} -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) -``` +Check out the translated languages either in the admin dashboard's settings or on [GitHub](https://github.com/medusajs/medusa/blob/develop/packages/admin/dashboard/src/i18n/languages.ts). *** -## Run Test +## How to Contribute Translation -Run the following command to run your tests: +1. Clone the [Medusa monorepository](https://github.com/medusajs/medusa) to your local machine: -```bash npm2yarn -npm run test:integration +```bash +git clone https://github.com/medusajs/medusa.git ``` -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). +If you already have it cloned, make sure to pull the latest changes from the `develop` branch. -This runs your Medusa application and runs the tests available under the `integrations/http` directory. +2. Install the monorepository's dependencies. Since it's a Yarn workspace, it's highly recommended to use yarn: -*** - -## 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") -}) +```bash +yarn install ``` -You can write the following test to ensure that the workflow throws that error: +3. Create a branch that you'll use to open the pull request later: -```ts title="integration-tests/http/workflow.spec.ts" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { helloWorldWorkflow } from "../../src/workflows/hello-world" +```bash +git checkout -b feat/translate- +``` -medusaIntegrationTestRunner({ - testSuite: ({ getContainer }) => { - describe("Test hello-world workflow", () => { - it("returns message", async () => { - const { errors } = await helloWorldWorkflow(getContainer()) - .run({ - throwOnError: false, - }) +Where `` is your language name. For example, `feat/translate-da`. - expect(errors.length).toBeGreaterThan(0) - expect(errors[0].error.message).toBe("Item doesn't exist") - }) - }) +4. Translation files are under `packages/admin/dashboard/src/i18n/translations` as JSON files whose names are the ISO-2 name of the language. + - If you're adding a new language, copy the file `packages/admin/dashboard/src/i18n/translations/en.json` and paste it with the ISO-2 name for your language. For example, if you're adding Danish translations, copy the `en.json` file and paste it as `packages/admin/dashboard/src/i18n/translations/de.json`. + - If you're fixing a translation, find the JSON file of the language under `packages/admin/dashboard/src/i18n/translations`. + +5. Start translating the keys in the JSON file (or updating the targeted ones). All keys in the JSON file must be translated, and your PR tests will fail otherwise. + - You can check whether the JSON file is valid by running the following command in `packages/admin/dashboard`, replacing `da.json` with the JSON file's name: + +```bash title="packages/admin/dashboard" +yarn i18n:validate da.json +``` + +6. After finishing the translation, if you're adding a new language, import its JSON file in `packages/admin/dashboard/src/i18n/translations/index.ts` and add it to the exported object: + +```ts title="packages/admin/dashboard/src/i18n/translations/index.ts" highlights={[["2"], ["6"], ["7"], ["8"]]} +// other imports... +import da from "./da.json" + +export default { + // other languages... + da: { + translation: da, }, -}) - -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. +The language's key in the object is the ISO-2 name of the language. -If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. +7. If you're adding a new language, add it to the file `packages/admin/dashboard/src/i18n/languages.ts`: + +```ts title="packages/admin/dashboard/src/i18n/languages.ts" highlights={languageHighlights} +import { da } from "date-fns/locale" +// other imports... + +export const languages: Language[] = [ + // other languages... + { + code: "da", + display_name: "Danish", + ltr: true, + date_locale: da, + }, +] +``` + +`languages` is an array having the following properties: + +- `code`: The ISO-2 name of the language. For example, `da` for Danish. +- `display_name`: The language's name to be displayed in the admin. +- `ltr`: Whether the language supports a left-to-right layout. For example, set this to `false` for languages like Arabic. +- `date_locale`: An instance of the locale imported from the [date-fns/locale](https://date-fns.org/) package. + +8. Once you're done, push the changes into your branch and open a pull request on GitHub. + +Our team will perform a general review on your PR and merge it if no issues are found. The translation will be available in the admin after the next release. # Example: Write Integration Tests for API Routes @@ -16671,98 +16634,135 @@ const response = await api.post(`/custom`, form, { ``` -# Translate Medusa Admin +# Example: Write Integration Tests for Workflows -The Medusa Admin supports multiple languages, with the default being English. In this documentation, you'll learn how to contribute to the community by translating the Medusa Admin to a language you're fluent in. +In this chapter, you'll learn 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. -{/* vale docs.We = NO */} +### Prerequisites -You can contribute either by translating the admin to a new language, or fixing translations for existing languages. As we can't validate every language's translations, some translations may be incorrect. Your contribution is welcome to fix any translation errors you find. +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -{/* vale docs.We = YES */} +## Write Integration Test for Workflow -Check out the translated languages either in the admin dashboard's settings or on [GitHub](https://github.com/medusajs/medusa/blob/develop/packages/admin/dashboard/src/i18n/languages.ts). +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) +``` *** -## How to Contribute Translation +## Run Test -1. Clone the [Medusa monorepository](https://github.com/medusajs/medusa) to your local machine: +Run the following command to run your tests: -```bash -git clone https://github.com/medusajs/medusa.git +```bash npm2yarn +npm run test:integration ``` -If you already have it cloned, make sure to pull the latest changes from the `develop` branch. +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). -2. Install the monorepository's dependencies. Since it's a Yarn workspace, it's highly recommended to use yarn: +This runs your Medusa application and runs the tests available under the `integrations/http` directory. -```bash -yarn install +*** + +## 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") +}) ``` -3. Create a branch that you'll use to open the pull request later: +You can write the following test to ensure that the workflow throws that error: -```bash -git checkout -b feat/translate- -``` +```ts title="integration-tests/http/workflow.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { helloWorldWorkflow } from "../../src/workflows/hello-world" -Where `` is your language name. For example, `feat/translate-da`. +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test hello-world workflow", () => { + it("returns message", async () => { + const { errors } = await helloWorldWorkflow(getContainer()) + .run({ + throwOnError: false, + }) -4. Translation files are under `packages/admin/dashboard/src/i18n/translations` as JSON files whose names are the ISO-2 name of the language. - - If you're adding a new language, copy the file `packages/admin/dashboard/src/i18n/translations/en.json` and paste it with the ISO-2 name for your language. For example, if you're adding Danish translations, copy the `en.json` file and paste it as `packages/admin/dashboard/src/i18n/translations/de.json`. - - If you're fixing a translation, find the JSON file of the language under `packages/admin/dashboard/src/i18n/translations`. - -5. Start translating the keys in the JSON file (or updating the targeted ones). All keys in the JSON file must be translated, and your PR tests will fail otherwise. - - You can check whether the JSON file is valid by running the following command in `packages/admin/dashboard`, replacing `da.json` with the JSON file's name: - -```bash title="packages/admin/dashboard" -yarn i18n:validate da.json -``` - -6. After finishing the translation, if you're adding a new language, import its JSON file in `packages/admin/dashboard/src/i18n/translations/index.ts` and add it to the exported object: - -```ts title="packages/admin/dashboard/src/i18n/translations/index.ts" highlights={[["2"], ["6"], ["7"], ["8"]]} -// other imports... -import da from "./da.json" - -export default { - // other languages... - da: { - translation: da, + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].error.message).toBe("Item doesn't exist") + }) + }) }, -} +}) + +jest.setTimeout(60 * 1000) ``` -The language's key in the object is the ISO-2 name of the language. +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. -7. If you're adding a new language, add it to the file `packages/admin/dashboard/src/i18n/languages.ts`: - -```ts title="packages/admin/dashboard/src/i18n/languages.ts" highlights={languageHighlights} -import { da } from "date-fns/locale" -// other imports... - -export const languages: Language[] = [ - // other languages... - { - code: "da", - display_name: "Danish", - ltr: true, - date_locale: da, - }, -] -``` - -`languages` is an array having the following properties: - -- `code`: The ISO-2 name of the language. For example, `da` for Danish. -- `display_name`: The language's name to be displayed in the admin. -- `ltr`: Whether the language supports a left-to-right layout. For example, set this to `false` for languages like Arabic. -- `date_locale`: An instance of the locale imported from the [date-fns/locale](https://date-fns.org/) package. - -8. Once you're done, push the changes into your branch and open a pull request on GitHub. - -Our team will perform a general review on your PR and merge it if no issues are found. The translation will be available in the admin after the next release. +If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. # Example: Integration Tests for a Module @@ -17015,6 +17015,156 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# Cart 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. + +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). + +## Cart 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. + +*** + +## 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) + + await cartModuleService.deleteCarts([cartId]) + } +) + +export const createCartWorkflow = createWorkflow( + "create-cart", + () => { + const { cart } = createCartStep() + + return new WorkflowResponse({ + cart, + }) + } +) +``` + +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 { createCartWorkflow } from "../../workflows/create-cart" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createCartWorkflow(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 { createCartWorkflow } from "../workflows/create-cart" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createCartWorkflow(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 { createCartWorkflow } from "../workflows/create-cart" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createCartWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + # 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. @@ -17286,6 +17436,150 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# 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). + +*** + + # 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. @@ -17452,298 +17746,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). - -*** - - -# 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). - -*** - - # 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. @@ -17900,156 +17902,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Cart 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. - -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). - -## Cart 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. - -*** - -## 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) - - await cartModuleService.deleteCarts([cartId]) - } -) - -export const createCartWorkflow = createWorkflow( - "create-cart", - () => { - const { cart } = createCartStep() - - return new WorkflowResponse({ - cart, - }) - } -) -``` - -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 { createCartWorkflow } from "../../workflows/create-cart" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createCartWorkflow(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 { createCartWorkflow } from "../workflows/create-cart" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createCartWorkflow(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 { createCartWorkflow } from "../workflows/create-cart" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createCartWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - # Payment 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. @@ -18359,149 +18211,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# 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. - -*** - -## 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 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - # Product 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. @@ -18656,6 +18365,149 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# 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. + +*** + +## 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 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + # Promotion 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. @@ -18964,6 +18816,154 @@ 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). + +*** + + # Stock Location 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. @@ -19101,6 +19101,150 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# Tax Module + +In this section of the documentation, you will find resources to learn more about the Tax Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. + +Medusa has tax related features available out-of-the-box through the Tax Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Tax Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Tax Features + +- [Tax Settings Per Region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md): Set different tax settings for each tax region. +- [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md): Manage each region's default tax rates and override them with conditioned tax rates. +- [Retrieve Tax Lines for carts and orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md): Calculate and retrieve the tax lines of a cart or order's line items and shipping methods with tax providers. + +*** + +## How to Use Tax Module's Service + +In your Medusa application, you build flows around commerce modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-tax-region.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createTaxRegionStep = createStep( + "create-tax-region", + async ({}, { container }) => { + const taxModuleService = container.resolve(Modules.TAX) + + const taxRegion = await taxModuleService.createTaxRegions({ + country_code: "us", + }) + + return new StepResponse({ taxRegion }, taxRegion.id) + }, + async (taxRegionId, { container }) => { + if (!taxRegionId) { + return + } + const taxModuleService = container.resolve(Modules.TAX) + + await taxModuleService.deleteTaxRegions([taxRegionId]) + } +) + +export const createTaxRegionWorkflow = createWorkflow( + "create-tax-region", + () => { + const { taxRegion } = createTaxRegionStep() + + return new WorkflowResponse({ taxRegion }) + } +) +``` + +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 { createTaxRegionWorkflow } from "../../workflows/create-tax-region" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createTaxRegionWorkflow(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 { createTaxRegionWorkflow } from "../workflows/create-tax-region" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createTaxRegionWorkflow(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 { createTaxRegionWorkflow } from "../workflows/create-tax-region" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createTaxRegionWorkflow(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). + +*** + +## 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. + +*** + + # Store 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. @@ -19242,6 +19386,169 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# 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: + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|| in |Stored|| + +*** + +## Sales Channel Module + +You can create a publishable API key and associate it with a sales channel. Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. + +![A diagram showcasing an example of how data models from the API Key and Sales Channel modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) + +This is useful to avoid passing the sales channel's ID as a parameter of every request, and instead pass the publishable API key in the header of any request to the Store API route. + +Learn more about this in the [Sales Channel Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). + +### Retrieve with Query + +To retrieve the 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", + }, +}) +``` + + +# 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. + + +# 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). + +![A diagram showcasing the relation between the Cart and Address data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711532392/Medusa%20Resources/cart-addresses_ls6qmv.jpg) + +*** + +## 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. + + # 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. @@ -19389,217 +19696,48 @@ The User Module accepts options for further configurations. Refer to [this docum *** -# Tax Module +# Links between Cart Module and Other Modules -In this section of the documentation, you will find resources to learn more about the Tax Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. - -Medusa has tax related features available out-of-the-box through the Tax Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in commerce modules, such as this Tax Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Tax Features - -- [Tax Settings Per Region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md): Set different tax settings for each tax region. -- [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md): Manage each region's default tax rates and override them with conditioned tax rates. -- [Retrieve Tax Lines for carts and orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md): Calculate and retrieve the tax lines of a cart or order's line items and shipping methods with tax providers. - -*** - -## How to Use Tax Module's Service - -In your Medusa application, you build flows around commerce modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-tax-region.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createTaxRegionStep = createStep( - "create-tax-region", - async ({}, { container }) => { - const taxModuleService = container.resolve(Modules.TAX) - - const taxRegion = await taxModuleService.createTaxRegions({ - country_code: "us", - }) - - return new StepResponse({ taxRegion }, taxRegion.id) - }, - async (taxRegionId, { container }) => { - if (!taxRegionId) { - return - } - const taxModuleService = container.resolve(Modules.TAX) - - await taxModuleService.deleteTaxRegions([taxRegionId]) - } -) - -export const createTaxRegionWorkflow = createWorkflow( - "create-tax-region", - () => { - const { taxRegion } = createTaxRegionStep() - - return new WorkflowResponse({ taxRegion }) - } -) -``` - -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 { createTaxRegionWorkflow } from "../../workflows/create-tax-region" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createTaxRegionWorkflow(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 { createTaxRegionWorkflow } from "../workflows/create-tax-region" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createTaxRegionWorkflow(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 { createTaxRegionWorkflow } from "../workflows/create-tax-region" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createTaxRegionWorkflow(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). - -*** - -## 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. +This document showcases the module links defined between the Cart Module and other commerce modules. ## Summary -The API Key Module has the following links to other modules: +The Cart 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. |First Data Model|Second Data Model|Type|Description| |---|---|---|---| +|| in |Read-only|| +| in ||Stored|| || in |Stored|| +|| in |Read-only|| +|| in |Read-only|| +|| in |Stored|| +|| in |Read-only|| +|| in |Read-only|| *** -## Sales Channel Module +## Customer Module -You can create a publishable API key and associate it with a sales channel. Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. - -![A diagram showcasing an example of how data models from the API Key and Sales Channel modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) - -This is useful to avoid passing the sales channel's ID as a parameter of every request, and instead pass the publishable API key in the header of any request to the Store API route. - -Learn more about this in the [Sales Channel Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). +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 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 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: apiKeys } = await query.graph({ - entity: "api_key", +const { data: carts } = await query.graph({ + entity: "cart", fields: [ - "sales_channels.*", + "customer.*", ], }) -// apiKeys.sales_channels +// carts.order ``` ### useQueryGraphStep @@ -19609,19 +19747,63 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: apiKeys } = useQueryGraphStep({ - entity: "api_key", +const { data: carts } = useQueryGraphStep({ + entity: "cart", fields: [ - "sales_channels.*", + "customer.*", ], }) -// apiKeys.sales_channels +// carts.order +``` + +*** + +## 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. + +Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. + +![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg) + +### 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`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "order.*", + ], +}) + +// carts.order +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "order.*", + ], +}) + +// carts.order ``` ### 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 order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -19631,11 +19813,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.API_KEY]: { - api_key_id: "apk_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", + [Modules.ORDER]: { + order_id: "order_123", }, }) ``` @@ -19649,15 +19831,427 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.API_KEY]: { - api_key_id: "apk_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", + [Modules.ORDER]: { + order_id: "order_123", }, }) ``` +*** + +## 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. + +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. + +![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) + +### 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`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "payment_collection.*", + ], +}) + +// carts.payment_collection +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "payment_collection.*", + ], +}) + +// carts.payment_collection +``` + +### 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): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` + +*** + +## Product Module + +Medusa defines read-only links between: + +- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `LineItem` data model. +- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `LineItem` data model. + +### Retrieve with Query + +To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: + +To retrieve the product, pass `product.*` in `fields`. + +### query.graph + +```ts +const { data: lineItems } = await query.graph({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems.variant +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: lineItems } = useQueryGraphStep({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems.variant +``` + +*** + +## Promotion Module + +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 diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) + +Medusa also defines a read-only link between the `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 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: carts } = await query.graph({ + entity: "cart", + fields: [ + "promotions.*", + ], +}) + +// carts.promotions +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "promotions.*", + ], +}) + +// carts.promotions +``` + +### 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", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +*** + +## Region Module + +Medusa defines a read-only link between the `Cart` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of a cart's region, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. + +### Retrieve with Query + +To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "region.*", + ], +}) + +// carts.region +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "region.*", + ], +}) + +// carts.region +``` + +*** + +## Sales Channel Module + +Medusa defines a read-only link between the `Cart` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of a cart's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. + +### Retrieve with Query + +To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) + +// carts.sales_channel +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) + +// carts.sales_channel +``` + + +# 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. + +![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg) + +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[] +) +``` + # Authentication Flows with the Auth Main Service @@ -19861,6 +20455,150 @@ 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. +# 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). + + +# 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. + +![A diagram showcasing the relation between other data models and the tax line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534431/Medusa%20Resources/cart-tax-lines_oheaq6.jpg) + +*** + +## 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. + +![A diagram showing an example of calculating the subtotal of a line item using its taxes](https://res.cloudinary.com/dza7lstvk/image/upload/v1711535295/Medusa%20Resources/cart-tax-inclusive_shpr3t.jpg) + +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 Providers In this document, you’ll learn how the Auth Module handles authentication using providers. @@ -20646,75 +21384,6 @@ In the workflow, you: You can use this workflow when deleting a manager, such as in an API route. -# 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). - - # Auth Module Options In this document, you'll learn about the options of the Auth Module. @@ -21095,601 +21764,6 @@ const { data: orders } = useQueryGraphStep({ ``` -# 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 diagram showcasing the relation between fulfillment sets, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712329770/Medusa%20Resources/service-zone_awmvfs.jpg) - -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. - - -# Fulfillment Module Provider - -In this document, you’ll learn what a fulfillment module provider is. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard. - -## What’s a Fulfillment Module Provider? - -A fulfillment module provider handles fulfilling items, typically using a third-party integration. - -Fulfillment module providers registered in the Fulfillment Module's [options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) are stored and represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md). - -*** - -## Configure Fulfillment Providers - -The Fulfillment Module accepts a `providers` option that allows you to register providers in your application. - -Learn more about the `providers` option in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md). - -*** - -## How to Create a Fulfillment Provider? - -Refer to [this guide](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) to learn how to create a fulfillment module provider. - - -# Item Fulfillment - -In this document, you’ll learn about the concepts of item fulfillment. - -## Fulfillment Data Model - -A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md). - -*** - -## Fulfillment Processing by a Fulfillment Provider - -A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items. - -The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. - -![A diagram showcasing the relation between a fulfillment, fulfillment provider, and shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331947/Medusa%20Resources/fulfillment-shipping-option_jk9ndp.jpg) - -*** - -## data Property - -The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. - -For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details. - -*** - -## Fulfillment Items - -A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model. - -The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill. - -![A diagram showcasing the relation between fulfillment and fulfillment items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712332114/Medusa%20Resources/fulfillment-item_etzxb0.jpg) - -*** - -## 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. - - -# 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: - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Stored|| -| in ||Stored|| -| in ||Stored|| -| in ||Stored|| -| in ||Stored|| -| in ||Stored|| - -*** - -## 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 diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg) - -A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. - -![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg) - -### 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`. - -### query.graph - -```ts -const { data: fulfillments } = await query.graph({ - entity: "fulfillment", - fields: [ - "order.*", - ], -}) - -// fulfillments.order -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: fulfillments } = useQueryGraphStep({ - entity: "fulfillment", - fields: [ - "order.*", - ], -}) - -// fulfillments.order -``` - -### 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): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", - }, -}) -``` - -### createRemoteLinkStep - -```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", - }, -}) -``` - -*** - -## 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. - -![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) - -### Retrieve with Query - -To retrieve the 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: shippingOptions } = await query.graph({ - entity: "shipping_option", - fields: [ - "price_set.*", - ], -}) - -// shippingOptions.price_set -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: shippingOptions } = useQueryGraphStep({ - entity: "shipping_option", - fields: [ - "price_set.*", - ], -}) - -// shippingOptions.price_set -``` - -### 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): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -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 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: shippingProfiles } = await query.graph({ - entity: "shipping_profile", - fields: [ - "products.*", - ], -}) - -// shippingProfiles.products -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: shippingProfiles } = useQueryGraphStep({ - entity: "shipping_profile", - fields: [ - "products.*", - ], -}) - -// 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", - }, -}) -``` - -*** - -## 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. - -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) - -Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. - -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg) - -### Retrieve with Query - -To retrieve the 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: fulfillmentSets } = await query.graph({ - entity: "fulfillment_set", - fields: [ - "location.*", - ], -}) - -// fulfillmentSets.location -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: fulfillmentSets } = useQueryGraphStep({ - entity: "fulfillment_set", - fields: [ - "location.*", - ], -}) - -// fulfillmentSets.location -``` - -### Manage with Link - -To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` - - -# Shipping Option - -In this document, you’ll learn about shipping options and their rules. - -## What’s a Shipping Option? - -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. - -A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). - -*** - -## Service Zone Restrictions - -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. - -![A diagram showcasing the relation between shipping options and service zones.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712330831/Medusa%20Resources/shipping-option-service-zone_pobh6k.jpg) - -Service zones can be more restrictive, such as restricting to certain cities or province codes. - -![A diagram showcasing the relation between shipping options, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331186/Medusa%20Resources/shipping-option-service-zone-city_m5sxod.jpg) - -*** - -## 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: - -- `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. - -![A diagram showcasing the relation between shipping option and shipping option rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331340/Medusa%20Resources/shipping-option-rule_oosopf.jpg) - -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. - -![A diagram showcasing how a shipping option can have multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331462/Medusa%20Resources/shipping-option-rule-2_ylaqdb.jpg) - -*** - -## Shipping Profile and Types - -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. - -*** - -## 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 `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. - - -# Fulfillment Module Options - -In this document, you'll learn about the options of the Fulfillment Module. - -## providers - -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. - -For example: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/fulfillment", - options: { - providers: [ - { - resolve: `@medusajs/medusa/fulfillment-manual`, - id: "manual", - options: { - // provider options... - }, - }, - ], - }, - }, - ], -}) -``` - -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 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. @@ -22323,43 +22397,180 @@ const { data: inventoryLevels } = useQueryGraphStep({ ``` -# Links between Currency Module and Other Modules +# Fulfillment Concepts -This document showcases the module links defined between the Currency Module and other commerce modules. +In this document, you’ll learn about some basic fulfillment concepts. -## Summary +## Fulfillment Set -The Currency Module has the following links to other modules: +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. -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +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. -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Read-only|| +```ts +const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( + [ + { + name: "Shipping", + type: "shipping", + }, + { + name: "Pick-up", + type: "pick-up", + }, + ] +) +``` *** -## Store Module +## Service Zone -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 service zone is a collection of geographical zones or areas. It’s used to restrict available shipping options to a defined set of locations. -Instead, Medusa defines a read-only link between the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `Currency` data model and the Currency Module's `Currency` data model. Because the link is read-only from the `Store`'s side, you can only retrieve the details of a store's supported currencies, and not the other way around. +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 diagram showcasing the relation between fulfillment sets, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712329770/Medusa%20Resources/service-zone_awmvfs.jpg) + +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. + + +# Fulfillment Module Provider + +In this document, you’ll learn what a fulfillment module provider is. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard. + +## What’s a Fulfillment Module Provider? + +A fulfillment module provider handles fulfilling items, typically using a third-party integration. + +Fulfillment module providers registered in the Fulfillment Module's [options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) are stored and represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md). + +*** + +## Configure Fulfillment Providers + +The Fulfillment Module accepts a `providers` option that allows you to register providers in your application. + +Learn more about the `providers` option in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md). + +*** + +## How to Create a Fulfillment Provider? + +Refer to [this guide](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) to learn how to create a fulfillment module provider. + + +# Item Fulfillment + +In this document, you’ll learn about the concepts of item fulfillment. + +## Fulfillment Data Model + +A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md). + +*** + +## Fulfillment Processing by a Fulfillment Provider + +A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items. + +The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. + +![A diagram showcasing the relation between a fulfillment, fulfillment provider, and shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331947/Medusa%20Resources/fulfillment-shipping-option_jk9ndp.jpg) + +*** + +## data Property + +The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. + +For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details. + +*** + +## Fulfillment Items + +A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model. + +The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill. + +![A diagram showcasing the relation between fulfillment and fulfillment items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712332114/Medusa%20Resources/fulfillment-item_etzxb0.jpg) + +*** + +## 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. + + +# 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: + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Stored|| +| in ||Stored|| +| in ||Stored|| +| in ||Stored|| +| in ||Stored|| +| in ||Stored|| + +*** + +## 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 diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg) + +A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. + +![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg) ### 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 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: stores } = await query.graph({ - entity: "store", +const { data: fulfillments } = await query.graph({ + entity: "fulfillment", fields: [ - "supported_currencies.currency.*", + "order.*", ], }) -// stores.supported_currencies +// fulfillments.order ``` ### useQueryGraphStep @@ -22369,69 +22580,371 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stores } = useQueryGraphStep({ - entity: "store", +const { data: fulfillments } = useQueryGraphStep({ + entity: "fulfillment", fields: [ - "supported_currencies.currency.*", + "order.*", ], }) -// stores.supported_currencies +// fulfillments.order +``` + +### 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): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", + }, +}) +``` + +### createRemoteLinkStep + +```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", + }, +}) +``` + +*** + +## 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. + +![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) + +### Retrieve with Query + +To retrieve the 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: shippingOptions } = await query.graph({ + entity: "shipping_option", + fields: [ + "price_set.*", + ], +}) + +// shippingOptions.price_set +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: shippingOptions } = useQueryGraphStep({ + entity: "shipping_option", + fields: [ + "price_set.*", + ], +}) + +// shippingOptions.price_set +``` + +### 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): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +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 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: shippingProfiles } = await query.graph({ + entity: "shipping_profile", + fields: [ + "products.*", + ], +}) + +// shippingProfiles.products +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: shippingProfiles } = useQueryGraphStep({ + entity: "shipping_profile", + fields: [ + "products.*", + ], +}) + +// 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", + }, +}) +``` + +*** + +## 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. + +![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) + +Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. + +![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg) + +### Retrieve with Query + +To retrieve the 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: fulfillmentSets } = await query.graph({ + entity: "fulfillment_set", + fields: [ + "location.*", + ], +}) + +// fulfillmentSets.location +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: fulfillmentSets } = useQueryGraphStep({ + entity: "fulfillment_set", + fields: [ + "location.*", + ], +}) + +// fulfillmentSets.location +``` + +### Manage with Link + +To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) ``` -# Order Claim +# Shipping Option -In this document, you’ll learn about order claims. +In this document, you’ll learn about shipping options and their rules. -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’s a Shipping Option? -## What is a Claim? +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 a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. +When the customer places their order, they choose a shipping option to be used to fulfill their items. -The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. +A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). *** -## Claim Type +## Service Zone Restrictions -The `Claim` data model has a `type` property whose value indicates the type of the claim: +A shipping option is restricted by a service zone, limiting the locations a shipping option be used in. -- `refund`: the items are returned, and the customer is refunded. -- `replace`: the items are returned, and the customer receives new items. +For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada. + +![A diagram showcasing the relation between shipping options and service zones.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712330831/Medusa%20Resources/shipping-option-service-zone_pobh6k.jpg) + +Service zones can be more restrictive, such as restricting to certain cities or province codes. + +![A diagram showcasing the relation between shipping options, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331186/Medusa%20Resources/shipping-option-service-zone-city_m5sxod.jpg) *** -## Old and Replacement Items +## Shipping Option Rules -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. +You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. -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). +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: -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). +- `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. + +![A diagram showcasing the relation between shipping option and shipping option rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331340/Medusa%20Resources/shipping-option-rule_oosopf.jpg) + +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. + +![A diagram showcasing how a shipping option can have multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331462/Medusa%20Resources/shipping-option-rule-2_ylaqdb.jpg) *** -## Claim Shipping Methods +## Shipping Profile and Types -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). +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). -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). +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. *** -## Claim Refund +## data Property -If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` 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 [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. +The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. # Order Concepts @@ -22540,6 +23053,60 @@ 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 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 Exchange In this document, you’ll learn about order exchanges. @@ -22593,6 +23160,210 @@ Any payment or refund made is stored in the [Transaction data model](https://doc When an exchange is confirmed, the order’s version is incremented. +# Fulfillment Module Options + +In this document, you'll learn about the options of the Fulfillment Module. + +## providers + +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. + +For example: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/fulfillment", + options: { + providers: [ + { + resolve: `@medusajs/medusa/fulfillment-manual`, + id: "manual", + options: { + // provider options... + }, + }, + ], + }, + }, + ], +}) +``` + +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 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\`| + + +# 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. + +![Diagram showcasing the automated RMA flow.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719578128/Medusa%20Resources/return-rma_pzprwq.jpg) + +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. + +## What are Tax Lines? + +A tax line indicates the tax rate of a line item or a shipping method. + +The [OrderLineItemTaxLine data model](https://docs.medusajs.com/references/order/models/OrderLineItemTaxLine/index.html.md) represents a line item’s tax line, and the [OrderShippingMethodTaxLine data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. + +![A diagram showcasing the relation between orders, items and shipping methods, and tax lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307225/Medusa%20Resources/order-tax-lines_sixujd.jpg) + +*** + +## Tax Inclusivity + +By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount and then adding it to the item/method’s subtotal. + +However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. + +So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. + +The following diagram is a simplified showcase of how a subtotal is calculated from the tax perspective. + +![A diagram showcasing how a subtotal is calculated from the tax perspective](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307395/Medusa%20Resources/order-tax-inclusive_oebdnm.jpg) + +For example, if a line item's amount is `5000`, the tax rate is `10`, and `is_tax_inclusive` is enabled, the tax amount is 10% of `5000`, which is `500`. The item's unit price becomes `4500`. + + # Links between Order Module and Other Modules This document showcases the module links defined between the Order Module and other commerce modules. @@ -23117,165 +23888,6 @@ const { data: orders } = useQueryGraphStep({ ``` -# 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\`| - - -# 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. - -![Diagram showcasing the automated RMA flow.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719578128/Medusa%20Resources/return-rma_pzprwq.jpg) - -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. - -## What are Tax Lines? - -A tax line indicates the tax rate of a line item or a shipping method. - -The [OrderLineItemTaxLine data model](https://docs.medusajs.com/references/order/models/OrderLineItemTaxLine/index.html.md) represents a line item’s tax line, and the [OrderShippingMethodTaxLine data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. - -![A diagram showcasing the relation between orders, items and shipping methods, and tax lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307225/Medusa%20Resources/order-tax-lines_sixujd.jpg) - -*** - -## Tax Inclusivity - -By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount and then adding it to the item/method’s subtotal. - -However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. - -So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. - -The following diagram is a simplified showcase of how a subtotal is calculated from the tax perspective. - -![A diagram showcasing how a subtotal is calculated from the tax perspective](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307395/Medusa%20Resources/order-tax-inclusive_oebdnm.jpg) - -For example, if a line item's amount is `5000`, the tax rate is `10`, and `is_tax_inclusive` is enabled, the tax amount is 10% of `5000`, which is `500`. The item's unit price becomes `4500`. - - # Transactions In this document, you’ll learn about an order’s transactions and its use. @@ -23446,673 +24058,59 @@ await orderModuleService.setOrderShippingMethodAdjustments( ``` -# Cart Concepts +# Payment Module Options -In this document, you’ll get an overview of the main concepts of a cart. +In this document, you'll learn about the options of the Payment Module. -## Shipping and Billing Addresses +## All Module Options -A cart has a shipping and billing address. Both of these addresses are represented by the [Address data model](https://docs.medusajs.com/references/cart/models/Address/index.html.md). - -![A diagram showcasing the relation between the Cart and Address data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711532392/Medusa%20Resources/cart-addresses_ls6qmv.jpg) +|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|-| *** -## Line Items +## providers Option -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. +The `providers` option is an array of payment module providers. -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. - -## 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. - -![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg) - -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). +When the Medusa application starts, these providers are registered and can be used to process payments. For example: -```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - ComputeActionAdjustmentLine, - ComputeActionItemLine, - ComputeActionShippingLine, +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ // ... -} 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. - -## Summary - -The Cart 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. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|| in |Read-only|| -| in ||Stored|| -|| in |Stored|| -|| in |Read-only|| -|| in |Read-only|| -|| in |Stored|| -|| in |Read-only|| -|| in |Read-only|| - -*** - -## 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. - -### 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`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "customer.*", - ], -}) - -// carts.order -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "customer.*", - ], -}) - -// carts.order -``` - -*** - -## 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. - -Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. - -![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg) - -### 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`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "order.*", - ], -}) - -// carts.order -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "order.*", - ], -}) - -// carts.order -``` - -### 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): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.ORDER]: { - order_id: "order_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.ORDER]: { - order_id: "order_123", - }, -}) -``` - -*** - -## 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. - -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. - -![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) - -### 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`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "payment_collection.*", - ], -}) - -// carts.payment_collection -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "payment_collection.*", - ], -}) - -// carts.payment_collection -``` - -### 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): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` - -*** - -## Product Module - -Medusa defines read-only links between: - -- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `LineItem` data model. -- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `LineItem` data model. - -### Retrieve with Query - -To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: - -To retrieve the product, pass `product.*` in `fields`. - -### query.graph - -```ts -const { data: lineItems } = await query.graph({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) - -// lineItems.variant -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: lineItems } = useQueryGraphStep({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) - -// lineItems.variant -``` - -*** - -## Promotion Module - -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 diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) - -Medusa also defines a read-only link between the `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 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: carts } = await query.graph({ - entity: "cart", - fields: [ - "promotions.*", - ], -}) - -// carts.promotions -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "promotions.*", - ], -}) - -// carts.promotions -``` - -### 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", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - -*** - -## Region Module - -Medusa defines a read-only link between the `Cart` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of a cart's region, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. - -### Retrieve with Query - -To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "region.*", - ], -}) - -// carts.region -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "region.*", - ], -}) - -// carts.region -``` - -*** - -## Sales Channel Module - -Medusa defines a read-only link between the `Cart` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of a cart's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. - -### Retrieve with Query - -To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) - -// carts.sales_channel -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) - -// carts.sales_channel -``` - - -# 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. - -![A diagram showcasing the relation between other data models and the tax line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534431/Medusa%20Resources/cart-tax-lines_oheaq6.jpg) - -*** - -## 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. - -![A diagram showing an example of calculating the subtotal of a line item using its taxes](https://res.cloudinary.com/dza7lstvk/image/upload/v1711535295/Medusa%20Resources/cart-tax-inclusive_shpr3t.jpg) - -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", + modules: [ + { + resolve: "@medusajs/medusa/payment", + options: { + providers: [ + { + resolve: "@medusajs/medusa/payment-stripe", + id: "stripe", + options: { + // ... + }, + }, + ], + }, }, - } -) + ], +}) ``` -Then, use the returned tax lines to set the line items and shipping methods’ tax lines: +The `providers` option is an array of objects that accept the following properties: -```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) -) -``` +- `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. # Account Holders and Saved Payment Methods @@ -24156,6 +24154,78 @@ Consequently, the Payment Module uses the payment provider to create an account This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). +# Payment + +In this document, you’ll learn what a payment is and how it's created, captured, and refunded. + +## What's a Payment? + +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. + +*** + +## 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. + +The payment can also be captured incrementally, each time a capture record is created for that amount. + +![A diagram showcasing how a payment's multiple captures are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565445/Medusa%20Resources/payment-capture_f5fve1.jpg) + +*** + +## Refund Payments + +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. + +A payment can be refunded multiple times, and each time a refund record is created. + +![A diagram showcasing how a payment's multiple refunds are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565555/Medusa%20Resources/payment-refund_lgfvyy.jpg) + + +# 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. + +![Diagram showcasing how a payment collection can have multiple payment sessions and payments](https://res.cloudinary.com/dza7lstvk/image/upload/v1711554695/Medusa%20Resources/payment-collection-multiple-payments_oi3z3n.jpg) + +*** + +## 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). + +![Diagram showcasing the relation between the Payment and Cart modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) + + # Links between Payment Module and Other Modules This document showcases the module links defined between the Payment Module and other commerce modules. @@ -24502,94 +24572,37 @@ createRemoteLinkStep({ ``` -# Payment Module Options +# Payment Session -In this document, you'll learn about the options of the Payment Module. +In this document, you’ll learn what a payment session is. -## All Module Options +## What's a Payment Session? -|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 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. + +![Diagram showcasing how every payment session has a different payment provider](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565056/Medusa%20Resources/payment-session-provider_guxzqt.jpg) *** -## providers Option +## data Property -The `providers` option is an array of payment module 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. -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" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/payment", - options: { - providers: [ - { - resolve: "@medusajs/medusa/payment-stripe", - id: "stripe", - 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. - - -# Payment - -In this document, you’ll learn what a payment is and how it's created, captured, and refunded. - -## What's a Payment? - -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. +For example, the customer's ID in Stripe is stored in the `data` property. *** -## Capture Payments +## Payment Session Status -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 `status` property of a payment session indicates its current status. Its value can be: -The payment can also be captured incrementally, each time a capture record is created for that amount. - -![A diagram showcasing how a payment's multiple captures are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565445/Medusa%20Resources/payment-capture_f5fve1.jpg) - -*** - -## Refund Payments - -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. - -A payment can be refunded multiple times, and each time a refund record is created. - -![A diagram showcasing how a payment's multiple refunds are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565555/Medusa%20Resources/payment-refund_lgfvyy.jpg) +- `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. # Accept Payment Flow @@ -24810,112 +24823,6 @@ 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 - -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. - -![Diagram showcasing how a payment collection can have multiple payment sessions and payments](https://res.cloudinary.com/dza7lstvk/image/upload/v1711554695/Medusa%20Resources/payment-collection-multiple-payments_oi3z3n.jpg) - -*** - -## 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). - -![Diagram showcasing the relation between the Payment and Cart modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) - - -# 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. - -![Diagram showcasing how every payment session has a different payment provider](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565056/Medusa%20Resources/payment-session-provider_guxzqt.jpg) - -*** - -## data Property - -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. - -![A diagram showcasing the steps of how the getWebhookActionAndData method words](https://res.cloudinary.com/dza7lstvk/image/upload/v1711567415/Medusa%20Resources/payment-webhook_seaocg.jpg) - -If the event's details indicate that the payment should be authorized, then the [authorizePaymentSession method of the main service](https://docs.medusajs.com/references/payment/authorizePaymentSession/index.html.md) is executed on the specified payment session. - -If the event's details indicate that the payment should be captured, then the [capturePayment method of the main service](https://docs.medusajs.com/references/payment/capturePayment/index.html.md) is executed on the payment of the specified payment session. - -### Actions After Webhook Payment Processing - -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. - - # Pricing Concepts In this document, you’ll learn about the main concepts in the Pricing Module. @@ -25123,6 +25030,42 @@ createRemoteLinkStep({ ``` +# Webhook Events + +In this document, you’ll learn how the Payment Module supports listening to webhook events. + +## What's a Webhook Event? + +A webhook event is sent from a third-party payment provider to your application. It indicates a change in a payment’s status. + +This is useful in many cases such as when a payment is being processed asynchronously or when a request is interrupted and the payment provider is sending details on the process later. + +*** + +## getWebhookActionAndData Method + +The Payment Module’s main service has a [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/getWebhookActionAndData/index.html.md) used to handle incoming webhook events from third-party payment services. The method delegates the handling to the associated payment provider, which returns the event's details. + +Medusa implements a webhook listener route at the `/hooks/payment/[identifier]_[provider]` API route, where: + +- `[identifier]` is the `identifier` static property defined in the payment provider. For example, `stripe`. +- `[provider]` is the ID of the provider. For example, `stripe`. + +For example, when integrating basic Stripe payments with the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md), the webhook listener route is `/hooks/payment/stripe_stripe`. If you're integrating Stripe's Bancontact payments, the webhook listener route is `/hooks/payment/stripe-bancontact_stripe`. + +Use that webhook listener in your third-party payment provider's configurations. + +![A diagram showcasing the steps of how the getWebhookActionAndData method words](https://res.cloudinary.com/dza7lstvk/image/upload/v1711567415/Medusa%20Resources/payment-webhook_seaocg.jpg) + +If the event's details indicate that the payment should be authorized, then the [authorizePaymentSession method of the main service](https://docs.medusajs.com/references/payment/authorizePaymentSession/index.html.md) is executed on the specified payment session. + +If the event's details indicate that the payment should be captured, then the [capturePayment method of the main service](https://docs.medusajs.com/references/payment/capturePayment/index.html.md) is executed on the payment of the specified payment session. + +### Actions After Webhook Payment Processing + +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. + + # 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. @@ -25415,186 +25358,6 @@ A region’s price preference’s `is_tax_inclusive`'s value takes higher preced - and the region has a price preference -# Links between Region Module and Other Modules - -This document showcases the module links defined between the Region Module and other commerce modules. - -## Summary - -The Region Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Read-only|| -| in ||Read-only|| -|| in |Stored|| - -*** - -## Cart Module - -Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Region` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the region of a cart, and not the other way around. - -### Retrieve with Query - -To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "region.*", - ], -}) - -// carts.region -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "region.*", - ], -}) - -// carts.region -``` - -*** - -## Order Module - -Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Region` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the region of an order, and not the other way around. - -### Retrieve with Query - -To retrieve the region of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: - -### query.graph - -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "region.*", - ], -}) - -// orders.region -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "region.*", - ], -}) - -// orders.region -``` - -*** - -## Payment Module - -You can specify for each region which payment providers are available for use. - -Medusa defines a module link between the `PaymentProvider` and the `Region` data models. - -![A diagram showcasing an example of how resources from the Payment and Region modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711569520/Medusa%20Resources/payment-region_jyo2dz.jpg) - -### Retrieve with Query - -To retrieve the payment providers of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_providers.*` in `fields`: - -### query.graph - -```ts -const { data: regions } = await query.graph({ - entity: "region", - fields: [ - "payment_providers.*", - ], -}) - -// regions.payment_providers -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: regions } = useQueryGraphStep({ - entity: "region", - fields: [ - "payment_providers.*", - ], -}) - -// regions.payment_providers -``` - -### Manage with Link - -To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.REGION]: { - region_id: "reg_123", - }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.REGION]: { - region_id: "reg_123", - }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", - }, -}) -``` - - # Links between Product Module and Other Modules This document showcases the module links defined between the Product Module and other commerce modules. @@ -26041,57 +25804,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 @@ -26158,6 +25870,237 @@ 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. + +## Summary + +The Region Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Read-only|| +| in ||Read-only|| +|| in |Stored|| + +*** + +## Cart Module + +Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Region` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the region of a cart, and not the other way around. + +### Retrieve with Query + +To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "region.*", + ], +}) + +// carts.region +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "region.*", + ], +}) + +// carts.region +``` + +*** + +## Order Module + +Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Region` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the region of an order, and not the other way around. + +### Retrieve with Query + +To retrieve the region of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: + +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "region.*", + ], +}) + +// orders.region +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "region.*", + ], +}) + +// orders.region +``` + +*** + +## Payment Module + +You can specify for each region which payment providers are available for use. + +Medusa defines a module link between the `PaymentProvider` and the `Region` data models. + +![A diagram showcasing an example of how resources from the Payment and Region modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711569520/Medusa%20Resources/payment-region_jyo2dz.jpg) + +### Retrieve with Query + +To retrieve the payment providers of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_providers.*` in `fields`: + +### query.graph + +```ts +const { data: regions } = await query.graph({ + entity: "region", + fields: [ + "payment_providers.*", + ], +}) + +// regions.payment_providers +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: regions } = useQueryGraphStep({ + entity: "region", + fields: [ + "payment_providers.*", + ], +}) + +// regions.payment_providers +``` + +### Manage with Link + +To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` + + # 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). @@ -26306,6 +26249,32 @@ 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. + +![A diagram showcasing the relation between the Campaign and Promotion data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899225/Medusa%20Resources/campagin-promotion_hh3qsi.jpg) + +*** + +## 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. + +![A diagram showcasing the relation between the Campaign and CampaignBudget data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899463/Medusa%20Resources/campagin-budget_rvqlmi.jpg) + + # Promotion Concepts In this document, you’ll learn about the main promotion and rule concepts in the Promotion Module. @@ -26363,32 +26332,6 @@ For example, to restrict the promotion to only `VIP` and `B2B` customer groups: In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion. -# 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. - -![A diagram showcasing the relation between the Campaign and Promotion data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899225/Medusa%20Resources/campagin-promotion_hh3qsi.jpg) - -*** - -## 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. - -![A diagram showcasing the relation between the Campaign and CampaignBudget data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899463/Medusa%20Resources/campagin-budget_rvqlmi.jpg) - - # Links between Promotion Module and Other Modules This document showcases the module links defined between the Promotion Module and other commerce modules. @@ -26944,6 +26887,63 @@ The Medusa application infers the associated sales channels and ensures that onl 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 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. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +| in ||Read-only|| + +*** + +## Store Module + +The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. + +Instead, Medusa defines a read-only link between the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `Currency` data model and the Currency Module's `Currency` data model. Because the link is read-only from the `Store`'s side, you can only retrieve the details of a store's supported currencies, and not the other way around. + +### 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 +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stores } = useQueryGraphStep({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) + +// stores.supported_currencies +``` + + # Stock Location Concepts In this document, you’ll learn about the main concepts in the Stock Location Module. @@ -27191,6 +27191,179 @@ createRemoteLinkStep({ ``` +# 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. + +## Tax Lines Calculation + +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. + +For example: + +```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", + }, + } +) +``` + +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. + +The example above retrieves the tax lines based on the tax region for the United States. + +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. */} + + +# Tax Rates and Rules + +In this document, you’ll learn about tax rates and rules. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions#manage-tax-rate-overrides/index.html.md) to learn how to manage tax rates using the dashboard. + +## What are Tax Rates? + +A tax rate is a percentage amount used to calculate the tax amount for each taxable item’s price, such as line items or shipping methods, in a cart. The sum of all calculated tax amounts are then added to the cart’s total as a tax total. + +Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. + +### Combinable Tax Rates + +Tax regions can have parent tax regions. To inherit the tax rates of the parent tax region, set the `is_combinable` of the child’s tax rates to `true`. + +Then, when tax rates are retrieved for a taxable item in the child region, both the child and the parent tax regions’ applicable rates are returned. + +*** + +## Override Tax Rates with Rules + +You can create tax rates that override the default for specific conditions or rules. + +For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15. + +A tax region can have multiple tax rates, and each tax rate can have multiple tax rules. The [TaxRateRule data model](https://docs.medusajs.com/references/tax/models/TaxRateRule/index.html.md) represents a tax rate’s rule. + +![A diagram showcasing the relation between TaxRegion, TaxRate, and TaxRateRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1711462167/Medusa%20Resources/tax-rate-rule_enzbp2.jpg) + +These two properties of the data model identify the rule’s target: + +- `reference`: the name of the table in the database that this rule points to. For example, `product_type`. +- `reference_id`: the ID of the data model’s record that this points to. For example, a product type’s ID. + +So, to override the default tax rate for product types “Shirt”, you create a tax rate and associate with it a tax rule whose `reference` is `product_type` and `reference_id` the ID of the “Shirt” product type. + + +# Tax 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. + + # Links between Store Module and Other Modules This document showcases the module links defined between the Store Module and other commerce modules. @@ -27365,179 +27538,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. - -## Tax Lines Calculation - -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. - -For example: - -```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", - }, - } -) -``` - -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. - -The example above retrieves the tax lines based on the tax region for the United States. - -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. */} - - -# Tax Rates and Rules - -In this document, you’ll learn about tax rates and rules. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions#manage-tax-rate-overrides/index.html.md) to learn how to manage tax rates using the dashboard. - -## What are Tax Rates? - -A tax rate is a percentage amount used to calculate the tax amount for each taxable item’s price, such as line items or shipping methods, in a cart. The sum of all calculated tax amounts are then added to the cart’s total as a tax total. - -Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. - -### Combinable Tax Rates - -Tax regions can have parent tax regions. To inherit the tax rates of the parent tax region, set the `is_combinable` of the child’s tax rates to `true`. - -Then, when tax rates are retrieved for a taxable item in the child region, both the child and the parent tax regions’ applicable rates are returned. - -*** - -## Override Tax Rates with Rules - -You can create tax rates that override the default for specific conditions or rules. - -For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15. - -A tax region can have multiple tax rates, and each tax rate can have multiple tax rules. The [TaxRateRule data model](https://docs.medusajs.com/references/tax/models/TaxRateRule/index.html.md) represents a tax rate’s rule. - -![A diagram showcasing the relation between TaxRegion, TaxRate, and TaxRateRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1711462167/Medusa%20Resources/tax-rate-rule_enzbp2.jpg) - -These two properties of the data model identify the rule’s target: - -- `reference`: the name of the table in the database that this rule points to. For example, `product_type`. -- `reference_id`: the ID of the data model’s record that this points to. For example, a product type’s ID. - -So, to override the default tax rate for product types “Shirt”, you create a tax rate and associate with it a tax rule whose `reference` is `product_type` and `reference_id` the ID of the “Shirt” product type. - - -# Tax 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. - - # Emailpass Auth Module Provider In this document, you’ll learn about the Emailpass auth module provider and how to install and use it in the Auth Module. @@ -28147,113 +28147,102 @@ For each product variant, you: ## Workflows - [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) -- [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/index.html.md) - [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/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) -- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) -- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/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) - [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/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) - [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) -- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) +- [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md) - [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/index.html.md) +- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) - [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md) -- [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md) - [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md) +- [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md) - [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md) -- [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md) -- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) - [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md) -- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) +- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/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) -- [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/index.html.md) +- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) - [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md) -- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md) +- [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/index.html.md) +- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md) +- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) +- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/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) - [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/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) +- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md) - [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) -- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md) -- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md) -- [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md) -- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md) -- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md) -- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) +- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/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) -- [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/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) -- [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) -- [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/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) -- [createReturnFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnFulfillmentWorkflow/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) - [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md) - [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md) -- [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/index.html.md) -- [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/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) +- [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/index.html.md) - [updateFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateFulfillmentWorkflow/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) +- [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/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) +- [updateShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingProfilesWorkflow/index.html.md) - [validateFulfillmentDeliverabilityStep](https://docs.medusajs.com/references/medusa-workflows/validateFulfillmentDeliverabilityStep/index.html.md) +- [batchInventoryItemLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchInventoryItemLevelsWorkflow/index.html.md) +- [bulkCreateDeleteLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/bulkCreateDeleteLevelsWorkflow/index.html.md) +- [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md) +- [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md) +- [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md) +- [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md) +- [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md) +- [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/index.html.md) +- [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) -- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/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) - [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) -- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/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) -- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md) -- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md) -- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) -- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) -- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/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) -- [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/index.html.md) -- [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/index.html.md) +- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/index.html.md) +- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/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) +- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md) +- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) - [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md) +- [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/index.html.md) +- [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/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) -- [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) +- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/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) -- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md) - [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md) -- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md) - [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md) -- [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md) -- [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md) +- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md) - [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/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) - [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md) - [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) - [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md) @@ -28262,200 +28251,203 @@ For each product variant, you: - [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) -- [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/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) +- [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/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) - [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md) - [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/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) +- [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) +- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/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) -- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) +- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) - [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) +- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) +- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/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) - [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md) -- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md) -- [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md) - [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md) - [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) - [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md) - [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/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) -- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) - [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md) -- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md) +- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) - [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md) - [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md) +- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md) - [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md) - [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md) +- [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/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) -- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md) +- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md) - [deleteOrderPaymentCollections](https://docs.medusajs.com/references/medusa-workflows/deleteOrderPaymentCollections/index.html.md) +- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md) - [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) - [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) -- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) - [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md) - [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md) +- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) - [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) +- [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) +- [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) -- [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/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) - [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/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) - [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md) -- [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md) +- [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md) - [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) -- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) - [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md) +- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) +- [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/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) - [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md) - [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md) +- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md) - [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md) - [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md) - [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md) +- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md) - [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md) - [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md) - [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md) -- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md) -- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/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) - [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) +- [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) - [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/index.html.md) -- [requestOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferValidationStep/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) - [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/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) +- [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) +- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) - [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md) - [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/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) +- [updateExchangeAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemWorkflow/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) - [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) - [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) -- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md) - [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md) - [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md) - [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md) -- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md) - [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md) -- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) -- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) -- [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md) -- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) - [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/index.html.md) -- [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/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) - [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) +- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md) +- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) +- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) +- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/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) +- [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/index.html.md) +- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md) +- [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/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) - [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) +- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/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) - [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) +- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/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) -- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md) +- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) - [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md) -- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/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) +- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/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) -- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md) -- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) - [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md) - [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md) -- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) -- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md) -- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) -- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md) -- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) -- [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) -- [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) -- [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/index.html.md) +- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) - [batchPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPromotionRulesWorkflow/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) - [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md) +- [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md) - [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/index.html.md) -- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md) -- [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/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) -- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md) +- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md) +- [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/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) +- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/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) - [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) +- [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/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) +- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) +- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md) +- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/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) - [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/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) -- [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) - [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md) @@ -28463,284 +28455,292 @@ For each product variant, you: - [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) +- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) +- [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md) +- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md) +- [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md) +- [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/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) - [createStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStoresWorkflow/index.html.md) - [deleteStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStoresWorkflow/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) - [updateStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStoresWorkflow/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) - [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) -- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/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) -- [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/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) -- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md) -- [maybeListTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/maybeListTaxRateRuleIdsStep/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) -- [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/index.html.md) +- [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/index.html.md) +- [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/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) ## Steps - [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) -- [updateApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateApiKeysStep/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) +- [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) +- [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) - [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) - [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/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) -- [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) -- [createPaymentCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentCollectionsStep/index.html.md) +- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md) - [createShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingMethodAdjustmentsStep/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) +- [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) -- [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/index.html.md) -- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/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) +- [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) +- [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) -- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) -- [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md) - [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) - [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md) - [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md) +- [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) -- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md) -- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md) -- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md) - [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/index.html.md) +- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md) - [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md) - [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md) - [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md) - [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md) - [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md) -- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md) - [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/index.html.md) +- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md) - [validateShippingStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingStep/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) -- [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/index.html.md) -- [updateRemoteLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRemoteLinksStep/index.html.md) -- [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/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) -- [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) -- [linkCustomersToCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomersToCustomerGroupStep/index.html.md) -- [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/index.html.md) -- [updateCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerGroupsStep/index.html.md) - [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) +- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md) - [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md) -- [uploadFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/uploadFilesStep/index.html.md) -- [deleteFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFilesStep/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) - [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) +- [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) -- [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md) - [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md) -- [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/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) -- [deleteShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionsStep/index.html.md) -- [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/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) -- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/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) +- [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) -- [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) -- [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) -- [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/index.html.md) -- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md) -- [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md) -- [deleteInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryLevelsStep/index.html.md) -- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md) -- [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/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) -- [deleteLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteLineItemsStep/index.html.md) +- [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/index.html.md) +- [attachInventoryItemToVariants](https://docs.medusajs.com/references/medusa-workflows/steps/attachInventoryItemToVariants/index.html.md) +- [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) +- [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) +- [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/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) - [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) - [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) +- [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) +- [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) +- [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) - [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) -- [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) +- [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) -- [createOrderExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangesStep/index.html.md) - [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/index.html.md) +- [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/index.html.md) - [createOrderExchangeItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangeItemsFromActionsStep/index.html.md) -- [createOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrdersStep/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) +- [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) - [declineOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/declineOrderChangeStep/index.html.md) +- [deleteExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteExchangesStep/index.html.md) - [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/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) - [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md) -- [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md) +- [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/index.html.md) - [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md) +- [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) - [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) +- [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/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) -- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) - [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/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) +- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) +- [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md) - [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/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) +- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md) +- [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/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) -- [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) -- [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/index.html.md) -- [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/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) -- [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) -- [updatePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesStep/index.html.md) -- [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/index.html.md) -- [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/index.html.md) -- [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/index.html.md) +- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/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) - [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) -- [removePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/removePriceListPricesStep/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) +- [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) +- [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) -- [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) -- [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) -- [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/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) -- [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/index.html.md) -- [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/index.html.md) -- [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md) -- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md) -- [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/index.html.md) -- [generateProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/generateProductCsvStep/index.html.md) -- [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md) -- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md) -- [groupProductsForBatchStep](https://docs.medusajs.com/references/medusa-workflows/steps/groupProductsForBatchStep/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) -- [updateProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTypesStep/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) -- [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) -- [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) +- [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md) +- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/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) +- [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/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) +- [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) +- [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md) +- [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/index.html.md) +- [generateProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/generateProductCsvStep/index.html.md) +- [groupProductsForBatchStep](https://docs.medusajs.com/references/medusa-workflows/steps/groupProductsForBatchStep/index.html.md) +- [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md) +- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/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) +- [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) - [addCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addCampaignPromotionsStep/index.html.md) +- [createCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCampaignsStep/index.html.md) +- [addRulesToPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addRulesToPromotionsStep/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) -- [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) +- [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/index.html.md) - [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/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) -- [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/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) -- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md) -- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md) -- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md) -- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md) - [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) -- [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/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) +- [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) +- [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) - [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md) +- [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md) - [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/index.html.md) -- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md) - [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/index.html.md) -- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/index.html.md) +- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md) +- [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/index.html.md) - [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) +- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/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) - [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/index.html.md) -- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/index.html.md) - [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/index.html.md) +- [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) +- [deleteShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingProfilesStep/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) -- [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) -- [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md) - [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md) +- [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/index.html.md) - [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md) - [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) - [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/index.html.md) -- [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/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) -- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/index.html.md) - [listTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateRuleIdsStep/index.html.md) -- [deleteUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteUsersStep/index.html.md) +- [listTaxRateIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateIdsStep/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) @@ -28767,6 +28767,84 @@ npx medusa --help *** +# build Command - Medusa CLI Reference + +Create a standalone build of the Medusa application. + +This creates a build that: + +- Doesn't rely on the source TypeScript files. +- Can be copied to a production server reliably. + +The build is outputted to a new `.medusa/server` directory. + +```bash +npx medusa build +``` + +Refer to [this section](#run-built-medusa-application) for next steps. + +## Options + +|Option|Description| +|---|---|---| +|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the | + +*** + +## Run Built Medusa Application + +After running the `build` command, use the following step to run the built Medusa application: + +- Change to the `.medusa/server` directory and install the dependencies: + +```bash npm2yarn +cd .medusa/server && npm install +``` + +- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead. + +```bash npm2yarn +cp .env .medusa/server/.env.production +``` + +- In the system environment variables, set `NODE_ENV` to `production`: + +```bash +NODE_ENV=production +``` + +- Use the `start` command to run the application: + +```bash npm2yarn +cd .medusa/server && npm run start +``` + +*** + +## Build Medusa Admin + +By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory. + +If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead. + + +# develop Command - Medusa CLI Reference + +Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. + +```bash +npx medusa develop +``` + +## Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`-H \\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \\`|Set port of the Medusa server.|\`9000\`| + + # db Commands - Medusa CLI Reference Commands starting with `db:` perform actions on the database. @@ -28887,84 +28965,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. - - -# develop Command - Medusa CLI Reference - -Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. - -```bash -npx medusa develop -``` - -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| - - # 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). @@ -28981,6 +28981,22 @@ npx medusa exec [file] [args...] |\`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 \\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \\`|Set port of the Medusa server.|\`9000\`| + + # new Command - Medusa CLI Reference Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project. @@ -29071,22 +29087,6 @@ npx medusa plugin:build ``` -# start Command - Medusa CLI Reference - -Start the Medusa application in production. - -```bash -npx medusa start -``` - -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| - - # 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). @@ -29106,6 +29106,22 @@ npx medusa start-cluster |\`-p \\`|Set port of the Medusa server.|\`9000\`| +# telemetry Command - Medusa CLI Reference + +Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. + +```bash +npx medusa telemetry +``` + +#### Options + +|Option|Description| +|---|---|---| +|\`--enable\`|Enable telemetry (default).| +|\`--disable\`|Disable telemetry.| + + # user Command - Medusa CLI Reference Create a new admin user. @@ -29125,22 +29141,6 @@ npx medusa user --email [--password ] If ran successfully, you'll receive the invite token in the output.|No|\`false\`| -# telemetry Command - Medusa CLI Reference - -Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. - -```bash -npx medusa telemetry -``` - -#### Options - -|Option|Description| -|---|---|---| -|\`--enable\`|Enable telemetry (default).| -|\`--disable\`|Disable telemetry.| - - # Medusa CLI Reference The Medusa CLI tool provides commands that facilitate your development. @@ -29164,68 +29164,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. @@ -29346,6 +29284,113 @@ 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. + + +# exec Command - Medusa CLI Reference + +Run a custom CLI script. Learn more about it in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md). + +```bash +npx medusa exec [file] [args...] +``` + +## Arguments + +|Argument|Description|Required| +|---|---|---|---|---| +|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes| +|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| + + +# new Command - Medusa CLI Reference + +Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project. + +```bash +medusa new [ []] +``` + +## Arguments + +|Argument|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-| +|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`| + +## Options + +|Option|Description| +|---|---|---| +|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.| +|\`--skip-db\`|Skip database creation.| +|\`--skip-env\`|Skip populating | +|\`--db-user \\`|The database user to use for database setup.| +|\`--db-database \\`|The name of the database used for database setup.| +|\`--db-pass \\`|The database password to use for database setup.| +|\`--db-port \\`|The database port to use for database setup.| +|\`--db-host \\`|The database host to use for database setup.| + + # 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. @@ -29362,6 +29407,57 @@ npx medusa develop |\`-p \\`|Set port of the Medusa server.|\`9000\`| +# start-cluster Command - Medusa CLI Reference + +Starts the Medusa application in [cluster mode](https://expressjs.com/en/advanced/best-practice-performance.html#run-your-app-in-a-cluster). + +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 \\`|The number of CPUs that Medusa can consume.|Medusa will try to consume all CPUs.| +|\`-H \\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \\`|Set port of the Medusa server.|\`9000\`| + + +# 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.| + + +# start Command - Medusa CLI Reference + +Start the Medusa application in production. + +```bash +npx medusa start +``` + +## Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`-H \\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \\`|Set port of the Medusa server.|\`9000\`| + + # plugin Commands - Medusa CLI Reference Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development. @@ -29423,73 +29519,6 @@ npx medusa plugin:build ``` -# start Command - Medusa CLI Reference - -Start the Medusa application in production. - -```bash -npx medusa start -``` - -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| - - -# 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 \\`|The number of CPUs that Medusa can consume.|Medusa will try to consume all CPUs.| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| - - -# 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.| - - -# 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| - - # user Command - Medusa CLI Reference Create a new admin user. @@ -29509,35 +29538,6 @@ npx medusa user --email [--password ] If ran successfully, you'll receive the invite token in the output.|No|\`false\`| -# new Command - Medusa CLI Reference - -Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project. - -```bash -medusa new [ []] -``` - -## Arguments - -|Argument|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-| -|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`| - -## Options - -|Option|Description| -|---|---|---| -|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.| -|\`--skip-db\`|Skip database creation.| -|\`--skip-env\`|Skip populating | -|\`--db-user \\`|The database user to use for database setup.| -|\`--db-database \\`|The name of the database used for database setup.| -|\`--db-pass \\`|The database password to use for database setup.| -|\`--db-port \\`|The database port to use for database setup.| -|\`--db-host \\`|The database host to use for database setup.| - - # Medusa JS SDK In this documentation, you'll learn how to install and use Medusa's JS SDK. @@ -29838,51 +29838,14 @@ 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) -- [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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.retrieve/index.html.md) -- [clearToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken/index.html.md) -- [clearToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken_/index.html.md) -- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md) -- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md) -- [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md) -- [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/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) -- [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) -- [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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.delete/index.html.md) -- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/index.html.md) -- [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) -- [getItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.getItem/index.html.md) -- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md) -- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/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) -- [batchCustomerGroups](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.batchCustomerGroups/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieve/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) +- [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) +- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md) - [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/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) -- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/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) - [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md) @@ -29890,168 +29853,205 @@ The object or class passed to `auth.storage` configuration must have the followi - [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) - [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/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) -- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md) - [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundItem/index.html.md) -- [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md) - [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundShipping/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md) +- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/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) +- [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) +- [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/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) +- [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/index.html.md) +- [setToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken/index.html.md) +- [setToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken_/index.html.md) +- [throwError\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.throwError_/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.delete/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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.list/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) +- [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) +- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/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/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) +- [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/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/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) +- [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) +- [batchCustomerGroups](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.batchCustomerGroups/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieve/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.create/index.html.md) +- [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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.retrieve/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) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/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/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.update/index.html.md) -- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/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) +- [listFulfillmentOptions](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.listFulfillmentOptions/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.list/index.html.md) - [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md) -- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/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) +- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/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) +- [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) - [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) -- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/index.html.md) -- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/index.html.md) -- [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md) - [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md) - [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md) - [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md) - [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md) -- [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/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) -- [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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.list/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/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) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md) +- [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/index.html.md) - [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) - [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) -- [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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.create/index.html.md) -- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.delete/index.html.md) -- [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md) -- [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md) +- [retrieveServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.retrieveServiceZone/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) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.retrieve/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) -- [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancel/index.html.md) -- [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/index.html.md) -- [createFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createFulfillment/index.html.md) -- [listChanges](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listChanges/index.html.md) -- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md) -- [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) -- [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) -- [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) -- [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/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) -- [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) -- [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) -- [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) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.list/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/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) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/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) +- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/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) +- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/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) +- [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) +- [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md) +- [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md) +- [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md) +- [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/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) +- [listChanges](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listChanges/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) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancel/index.html.md) +- [requestTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.requestTransfer/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) +- [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) +- [retrievePreview](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrievePreview/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/index.html.md) +- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md) +- [markAsPaid](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.markAsPaid/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) +- [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) +- [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md) +- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md) +- [batchPrices](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.batchPrices/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/PriceList/methods/js_sdk.admin.PriceList.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md) +- [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/index.html.md) +- [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) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md) +- [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) +- [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) +- [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/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/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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.delete/index.html.md) +- [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/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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/index.html.md) +- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.update/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) -- [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) -- [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) -- [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) +- [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/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) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/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) - [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md) -- [updateVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateVariant/index.html.md) +- [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) -- [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) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.list/index.html.md) -- [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/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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.delete/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/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) -- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.updateProducts/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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.update/index.html.md) +- [updateVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateVariant/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/ProductVariant/methods/js_sdk.admin.ProductVariant.list/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) +- [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/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md) - [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/index.html.md) - [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md) +- [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/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) - [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) - [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) - [update](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.update/index.html.md) @@ -30060,80 +30060,80 @@ The object or class passed to `auth.storage` configuration must have the followi - [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) - [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) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/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) +- [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) +- [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) - [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/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) -- [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/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) - [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md) - [removeDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeDismissItem/index.html.md) - [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md) -- [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/index.html.md) +- [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/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) +- [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) -- [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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.delete/index.html.md) - [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.create/index.html.md) +- [updateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateRequest/index.html.md) +- [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/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) - [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/index.html.md) +- [batchProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.batchProducts/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.create/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.list/index.html.md) - [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) - [update](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.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) +- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.updateProducts/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/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) +- [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) - [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/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) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md) +- [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/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) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.list/index.html.md) - [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md) - [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md) +- [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) -- [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/TaxRate/methods/js_sdk.admin.TaxRate.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md) -- [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/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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/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) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/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) +- [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) - [list](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.retrieve/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.delete/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/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.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) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.retrieve/index.html.md) @@ -30141,9 +30141,9 @@ The object or class passed to `auth.storage` configuration must have the followi - [callback](https://docs.medusajs.com/references/js-sdk/auth/callback/index.html.md) - [logout](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md) -- [login](https://docs.medusajs.com/references/js-sdk/auth/login/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) - [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) @@ -30152,8 +30152,8 @@ 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) -- [customer](https://docs.medusajs.com/references/js-sdk/store/customer/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) @@ -30826,65 +30826,6 @@ These layout components allow you to set the layout of your [UI routes](https:// These components allow you to use common Medusa Admin components in your custom UI routes and widgets. -# Container - Admin Components - -The Medusa Admin wraps each section of a page in a container. - -![Example of a container in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728287102/Medusa%20Resources/container_soenir.png) - -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 - -export const Container = (props: ContainerProps) => { - return ( - - ) -} -``` - -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 ( - -
- - ) -} - -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. @@ -31110,111 +31051,39 @@ export default ProductWidget ``` -# Header - Admin Components +# Container - Admin Components -Each section in the Medusa Admin has a header with a title, and optionally a subtitle with buttons to perform an action. +The Medusa Admin wraps each section of a page in a container. -![Example of a header in a section](https://res.cloudinary.com/dza7lstvk/image/upload/v1728288562/Medusa%20Resources/header_dtz4gl.png) +![Example of a container in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728287102/Medusa%20Resources/container_soenir.png) -To create a component that uses the same header styling and structure, create the file `src/admin/components/header.tsx` with the following content: +To create a component that uses the same container styling in your widgets or UI routes, create the file `src/admin/components/container.tsx` with the following content: -```tsx title="src/admin/components/header.tsx" -import { Heading, Button, Text } from "@medusajs/ui" -import React from "react" -import { Link, LinkProps } from "react-router-dom" -import { ActionMenu, ActionMenuProps } from "./action-menu" +```tsx +import { + Container as UiContainer, + clx, +} from "@medusajs/ui" -export type HeadingProps = { - title: string - subtitle?: string - actions?: ( - { - type: "button", - props: React.ComponentProps - link?: LinkProps - } | - { - type: "action-menu" - props: ActionMenuProps - } | - { - type: "custom" - children: React.ReactNode - } - )[] -} +type ContainerProps = React.ComponentProps -export const Header = ({ - title, - subtitle, - actions = [], -}: HeadingProps) => { +export const Container = (props: ContainerProps) => { return ( -
-
- {title} - {subtitle && ( - - {subtitle} - - )} -
- {actions.length > 0 && ( -
- {actions.map((action, index) => ( - <> - {action.type === "button" && ( - - )} - {action.type === "action-menu" && ( - - )} - {action.type === "custom" && action.children} - - ))} -
- )} -
+ ) } ``` -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. +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 `Header` 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: @@ -31226,22 +31095,7 @@ import { Header } from "../components/header" const ProductWidget = () => { return ( -
{ - alert("You clicked the button.") - }, - }, - }, - ]} - /> +
) } @@ -31253,722 +31107,7 @@ 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. - - -# Data Table - Admin Components - -This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). - -The [DataTable component in Medusa UI](https://docs.medusajs.com/ui/components/data-table/index.html.md) allows you to display data in a table with sorting, filtering, and pagination. It's used across the Medusa Admin dashboard to showcase a list of items, such as a list of products. - -![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png) - -You can use this component in your Admin Extensions to display data in a table format, especially if you're retrieving them from API routes of the Medusa application. - -This guide focuses on how to use the `DataTable` component while fetching data from the backend. Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/components/data-table/index.html.md) for detailed information about the DataTable component and its different usages. - -## Example: DataTable with Data Fetching - -In this example, you'll create a UI widget that shows the list of products retrieved from the [List Products API Route](https://docs.medusajs.com/api/admin#products_getproducts) in a data table with pagination, filtering, searching, and sorting. - -Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI: - -```tsx title="src/admin/routes/custom/page.tsx" -import { - createDataTableColumnHelper, -} from "@medusajs/ui" -import { - HttpTypes, -} from "@medusajs/framework/types" - -const columnHelper = createDataTableColumnHelper() - -const columns = [ - columnHelper.accessor("title", { - header: "Title", - // Enables sorting for the column. - enableSorting: true, - // If omitted, the header will be used instead if it's a string, - // otherwise the accessor key (id) will be used. - sortLabel: "Title", - // If omitted the default value will be "A-Z" - sortAscLabel: "A-Z", - // If omitted the default value will be "Z-A" - sortDescLabel: "Z-A", - }), - columnHelper.accessor("status", { - header: "Status", - cell: ({ getValue }) => { - const status = getValue() - return ( - - {status === "published" ? "Published" : "Draft"} - - ) - }, - }), -] -``` - -`createDataTableColumnHelper` utility creates a column helper that helps you define the columns for the data table. The column helper has an `accessor` method that accepts two parameters: - -1. The column's key in the table's data. -2. An object with the following properties: - - `header`: The column's header. - - `cell`: (optional) By default, a data's value for a column is displayed as a string. Use this property to specify custom rendering of the value. It accepts a function that returns a string or a React node. The function receives an object that has a `getValue` property function to retrieve the raw value of the cell. - - `enableSorting`: (optional) A boolean that enables sorting data by this column. - - `sortLabel`: (optional) The label for the sorting button. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used. - - `sortAscLabel`: (optional) The label for the ascending sorting button. If omitted, the default value will be "A-Z". - - `sortDescLabel`: (optional) The label for the descending sorting button. If omitted, the default value will be "Z-A". - -Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status. - -To define the filters, add the following: - -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { - // ... - createDataTableFilterHelper, -} from "@medusajs/ui" - -const filterHelper = createDataTableFilterHelper() - -const filters = [ - filterHelper.accessor("status", { - 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({ - pageSize: limit, - pageIndex: 0, - }) - const [search, setSearch] = useState("") - const [filtering, setFiltering] = useState({}) - const [sorting, setSorting] = useState(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 ( - - - - - Products -
- - - -
-
- - -
-
-
-) -``` - -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() - -const columns = [ - columnHelper.accessor("title", { - header: "Title", - // Enables sorting for the column. - enableSorting: true, - // If omitted, the header will be used instead if it's a string, - // otherwise the accessor key (id) will be used. - sortLabel: "Title", - // If omitted the default value will be "A-Z" - sortAscLabel: "A-Z", - // If omitted the default value will be "Z-A" - sortDescLabel: "Z-A", - }), - columnHelper.accessor("status", { - header: "Status", - cell: ({ getValue }) => { - const status = getValue() - return ( - - {status === "published" ? "Published" : "Draft"} - - ) - }, - }), -] - -const filterHelper = createDataTableFilterHelper() - -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({ - pageSize: limit, - pageIndex: 0, - }) - const [search, setSearch] = useState("") - const [filtering, setFiltering] = useState({}) - const [sorting, setSorting] = useState(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 ( - - - - - Products -
- - - -
-
- - -
-
-
- ) -} - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - - -# JSON View - Admin Components - -Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format. - -![Example of a JSON section in the admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295129/Medusa%20Resources/json_dtbsgm.png) - -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 ( - -
- JSON - - {numberOfKeys} keys - -
- - - - - - - -
-
- - - - {numberOfKeys} - - - -
-
- - esc - - - - - - -
-
- -
-
} - > - - } /> - ( - null - )} - /> - ( - undefined - )} - /> - { - return ( - - {Object.keys(value as object).length} items - - ) - }} - /> - - - - - : - - { - return - }} - /> - - - -
-
-
-
- ) -} - -type CopiedProps = { - style?: CSSProperties - value: object | undefined -} - -const Copied = ({ style, value }: CopiedProps) => { - const [copied, setCopied] = useState(false) - - const handler = (e: MouseEvent) => { - 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 ( - - - - ) - } - - return ( - - - - ) -} -``` - -The `JsonViewSection` component shows a section with the "JSON" title and a button to show the data as JSON in a drawer or side window. - -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 -} - -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" }`. +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. # Forms - Admin Components @@ -32544,6 +31683,380 @@ 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 + +Each section in the Medusa Admin has a header with a title, and optionally a subtitle with buttons to perform an action. + +![Example of a header in a section](https://res.cloudinary.com/dza7lstvk/image/upload/v1728288562/Medusa%20Resources/header_dtz4gl.png) + +To create a component that uses the same header styling and structure, create the file `src/admin/components/header.tsx` with the following content: + +```tsx title="src/admin/components/header.tsx" +import { Heading, Button, Text } from "@medusajs/ui" +import React from "react" +import { Link, LinkProps } from "react-router-dom" +import { ActionMenu, ActionMenuProps } from "./action-menu" + +export type HeadingProps = { + title: string + subtitle?: string + actions?: ( + { + type: "button", + props: React.ComponentProps + link?: LinkProps + } | + { + type: "action-menu" + props: ActionMenuProps + } | + { + type: "custom" + children: React.ReactNode + } + )[] +} + +export const Header = ({ + title, + subtitle, + actions = [], +}: HeadingProps) => { + return ( +
+
+ {title} + {subtitle && ( + + {subtitle} + + )} +
+ {actions.length > 0 && ( +
+ {actions.map((action, index) => ( + <> + {action.type === "button" && ( + + )} + {action.type === "action-menu" && ( + + )} + {action.type === "custom" && action.children} + + ))} +
+ )} +
+ ) +} +``` + +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 ( + +
{ + alert("You clicked the button.") + }, + }, + }, + ]} + /> + + ) +} + +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. + + +# JSON View - Admin Components + +Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format. + +![Example of a JSON section in the admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295129/Medusa%20Resources/json_dtbsgm.png) + +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 ( + +
+ JSON + + {numberOfKeys} keys + +
+ + + + + + + +
+
+ + + + {numberOfKeys} + + + +
+
+ + esc + + + + + + +
+
+ +
+
} + > + + } /> + ( + null + )} + /> + ( + undefined + )} + /> + { + return ( + + {Object.keys(value as object).length} items + + ) + }} + /> + + + + + : + + { + return + }} + /> + + + +
+
+
+
+ ) +} + +type CopiedProps = { + style?: CSSProperties + value: object | undefined +} + +const Copied = ({ style, value }: CopiedProps) => { + const [copied, setCopied] = useState(false) + + const handler = (e: MouseEvent) => { + 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 ( + + + + ) + } + + return ( + + + + ) +} +``` + +The `JsonViewSection` component shows a section with the "JSON" title and a button to show the data as JSON in a drawer or side window. + +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 +} + +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" }`. + + # Section Row - Admin Components The Medusa Admin often shows information in rows of label-values, such as when showing a product's details. @@ -32636,6 +32149,632 @@ 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. +# 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. + +![An example of an admin page with a single column](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286605/Medusa%20Resources/single-column.png) + +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 ( +
+ {children} +
+ ) +} +``` + +The `SingleColumnLayout` accepts the content in the `children` props. + +*** + +## Example + +Use the `SingleColumnLayout` 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 { SingleColumnLayout } from "../../layouts/single-column" +import { Header } from "../../components/header" + +const CustomPage = () => { + return ( + + +
+ + + ) +} + +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) + +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. + + +# Two Column Layout - Admin Components + +The Medusa Admin has pages with two columns of content. + +This doesn't include the sidebar, only the main content. + +![An example of an admin page with two columns](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286690/Medusa%20Resources/two-column_sdnkg0.png) + +To create a layout that you can use in UI routes to support two columns of content, create the component `src/admin/layouts/two-column.tsx` with the following content: + +```tsx title="src/admin/layouts/two-column.tsx" +export type TwoColumnLayoutProps = { + firstCol: React.ReactNode + secondCol: React.ReactNode +} + +export const TwoColumnLayout = ({ + firstCol, + secondCol, +}: TwoColumnLayoutProps) => { + return ( +
+
+ {firstCol} +
+
+ {secondCol} +
+
+ ) +} +``` + +The `TwoColumnLayout` accepts two props: + +- `firstCol` indicating the content of the first column. +- `secondCol` indicating the content of the second column. + +*** + +## Example + +Use the `TwoColumnLayout` component in your UI routes that have a single column. For example: + +```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]} +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" +import { Container } from "../../components/container" +import { Header } from "../../components/header" +import { TwoColumnLayout } from "../../layouts/two-column" + +const CustomPage = () => { + return ( + +
+ + } + secondCol={ + +
+ + } + /> + ) +} + +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) + +export default CustomPage +``` + +This UI route also uses [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header]() custom components. + + +# Data Table - Admin Components + +This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). + +The [DataTable component in Medusa UI](https://docs.medusajs.com/ui/components/data-table/index.html.md) allows you to display data in a table with sorting, filtering, and pagination. It's used across the Medusa Admin dashboard to showcase a list of items, such as a list of products. + +![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png) + +You can use this component in your Admin Extensions to display data in a table format, especially if you're retrieving them from API routes of the Medusa application. + +This guide focuses on how to use the `DataTable` component while fetching data from the backend. Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/components/data-table/index.html.md) for detailed information about the DataTable component and its different usages. + +## Example: DataTable with Data Fetching + +In this example, you'll create a UI widget that shows the list of products retrieved from the [List Products API Route](https://docs.medusajs.com/api/admin#products_getproducts) in a data table with pagination, filtering, searching, and sorting. + +Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI: + +```tsx title="src/admin/routes/custom/page.tsx" +import { + createDataTableColumnHelper, +} from "@medusajs/ui" +import { + HttpTypes, +} from "@medusajs/framework/types" + +const columnHelper = createDataTableColumnHelper() + +const columns = [ + columnHelper.accessor("title", { + header: "Title", + // Enables sorting for the column. + enableSorting: true, + // If omitted, the header will be used instead if it's a string, + // otherwise the accessor key (id) will be used. + sortLabel: "Title", + // If omitted the default value will be "A-Z" + sortAscLabel: "A-Z", + // If omitted the default value will be "Z-A" + sortDescLabel: "Z-A", + }), + columnHelper.accessor("status", { + header: "Status", + cell: ({ getValue }) => { + const status = getValue() + return ( + + {status === "published" ? "Published" : "Draft"} + + ) + }, + }), +] +``` + +`createDataTableColumnHelper` utility creates a column helper that helps you define the columns for the data table. The column helper has an `accessor` method that accepts two parameters: + +1. The column's key in the table's data. +2. An object with the following properties: + - `header`: The column's header. + - `cell`: (optional) By default, a data's value for a column is displayed as a string. Use this property to specify custom rendering of the value. It accepts a function that returns a string or a React node. The function receives an object that has a `getValue` property function to retrieve the raw value of the cell. + - `enableSorting`: (optional) A boolean that enables sorting data by this column. + - `sortLabel`: (optional) The label for the sorting button. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used. + - `sortAscLabel`: (optional) The label for the ascending sorting button. If omitted, the default value will be "A-Z". + - `sortDescLabel`: (optional) The label for the descending sorting button. If omitted, the default value will be "Z-A". + +Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status. + +To define the filters, add the following: + +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { + // ... + createDataTableFilterHelper, +} from "@medusajs/ui" + +const filterHelper = createDataTableFilterHelper() + +const filters = [ + filterHelper.accessor("status", { + 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({ + pageSize: limit, + pageIndex: 0, + }) + const [search, setSearch] = useState("") + const [filtering, setFiltering] = useState({}) + const [sorting, setSorting] = useState(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 ( + + + + + Products +
+ + + +
+
+ + +
+
+
+) +``` + +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() + +const columns = [ + columnHelper.accessor("title", { + header: "Title", + // Enables sorting for the column. + enableSorting: true, + // If omitted, the header will be used instead if it's a string, + // otherwise the accessor key (id) will be used. + sortLabel: "Title", + // If omitted the default value will be "A-Z" + sortAscLabel: "A-Z", + // If omitted the default value will be "Z-A" + sortDescLabel: "Z-A", + }), + columnHelper.accessor("status", { + header: "Status", + cell: ({ getValue }) => { + const status = getValue() + return ( + + {status === "published" ? "Published" : "Draft"} + + ) + }, + }), +] + +const filterHelper = createDataTableFilterHelper() + +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({ + pageSize: limit, + pageIndex: 0, + }) + const [search, setSearch] = useState("") + const [filtering, setFiltering] = useState({}) + const [sorting, setSorting] = useState(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 ( + + + + + Products +
+ + + +
+
+ + +
+
+
+ ) +} + +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) + +export default CustomPage +``` + + # 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. @@ -32926,145 +33065,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. -# Two Column Layout - Admin Components - -The Medusa Admin has pages with two columns of content. - -This doesn't include the sidebar, only the main content. - -![An example of an admin page with two columns](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286690/Medusa%20Resources/two-column_sdnkg0.png) - -To create a layout that you can use in UI routes to support two columns of content, create the component `src/admin/layouts/two-column.tsx` with the following content: - -```tsx title="src/admin/layouts/two-column.tsx" -export type TwoColumnLayoutProps = { - firstCol: React.ReactNode - secondCol: React.ReactNode -} - -export const TwoColumnLayout = ({ - firstCol, - secondCol, -}: TwoColumnLayoutProps) => { - return ( -
-
- {firstCol} -
-
- {secondCol} -
-
- ) -} -``` - -The `TwoColumnLayout` accepts two props: - -- `firstCol` indicating the content of the first column. -- `secondCol` indicating the content of the second column. - -*** - -## Example - -Use the `TwoColumnLayout` component in your UI routes that have a single column. For example: - -```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]} -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" -import { Container } from "../../components/container" -import { Header } from "../../components/header" -import { TwoColumnLayout } from "../../layouts/two-column" - -const CustomPage = () => { - return ( - -
- - } - secondCol={ - -
- - } - /> - ) -} - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - -This UI route also uses [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header]() custom components. - - -# 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. - -![An example of an admin page with a single column](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286605/Medusa%20Resources/single-column.png) - -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 ( -
- {children} -
- ) -} -``` - -The `SingleColumnLayout` accepts the content in the `children` props. - -*** - -## Example - -Use the `SingleColumnLayout` 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 { SingleColumnLayout } from "../../layouts/single-column" -import { Header } from "../../components/header" - -const CustomPage = () => { - return ( - - -
- - - ) -} - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -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. - - # 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. @@ -33092,149 +33092,6 @@ Some examples of method names: 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) - -const posts = await postModuleService.listPosts({ - published_at: { - $gte: startToday, - $lte: endToday, - }, -}) -``` - -The `dateTime` property also stores the time. So, when matching for an exact day, you must set a range filter to be between the beginning and end of the day. - -In this example, you retrieve the current date twice: once to set its time to `00:00:00`, and another to set its time `23:59:59`. Then, you retrieve posts whose `published_at` property is between `00:00:00` and `23:59:59` of today. - -*** - -## Apply Or Condition - -```ts -const posts = await postModuleService.listPosts({ - $or: [ - { - name: "My Post", - }, - { - published_at: { - $lt: new Date(), - }, - }, - ], -}) -``` - -To use an `or` condition, pass to the filter object the `$or` property, whose value is an array of filters. - -In the example above, posts whose name is `My Post` or their `published_at` date is less than the current date and time are retrieved. - - # create Method - Service Factory Reference This method creates one or more records of the data model. @@ -33273,45 +33130,92 @@ 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 +# restore Method - Service Factory Reference -This method deletes one or more records. +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). -## Delete One Record +## Restore One Record ```ts -await postModuleService.deletePosts("123") +const restoredPosts = await postModuleService.restorePosts("123") ``` -To delete one record, pass its ID as a parameter of the method. +### Parameters + +To restore one record, pass its ID as a parameter of the method. + +### Returns + +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. + +For example, the returned object of the above example is: + +```ts +restoredPosts = { + post_id: ["123"], +} +``` *** -## Delete Multiple Records +## Restore Multiple Records ```ts -await postModuleService.deletePosts([ +const restoredPosts = await postModuleService.restorePosts([ "123", "321", ]) ``` -To delete multiple records, pass an array of IDs as a parameter of the method. +### Parameters + +To restore multiple records, pass an array of IDs as a parameter of the method. + +### Returns + +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. + +For example, the returned object of the above example is: + +```ts +restoredPosts = { + post_id: [ + "123", + "321", + ], +} +``` *** -## Delete Records Matching Filters +## Restore Records Matching Filters ```ts -await postModuleService.deletePosts({ +const restoredPosts = await postModuleService.restorePosts({ name: "My Post", }) ``` -To delete records matching a set of filters, pass an object of filters as a parameter. +### 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", + ], +} +``` + # listAndCount Method - Service Factory Reference @@ -33449,124 +33353,6 @@ The method returns an array with two items: 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. - - # retrieve Method - Service Factory Reference This method retrieves one record of the data model by its ID. @@ -33711,6 +33497,149 @@ deletedPosts = { ``` +# 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) + +const posts = await postModuleService.listPosts({ + published_at: { + $gte: startToday, + $lte: endToday, + }, +}) +``` + +The `dateTime` property also stores the time. So, when matching for an exact day, you must set a range filter to be between the beginning and end of the day. + +In this example, you retrieve the current date twice: once to set its time to `00:00:00`, and another to set its time `23:59:59`. Then, you retrieve posts whose `published_at` property is between `00:00:00` and `23:59:59` of today. + +*** + +## Apply Or Condition + +```ts +const posts = await postModuleService.listPosts({ + $or: [ + { + name: "My Post", + }, + { + published_at: { + $lt: new Date(), + }, + }, + ], +}) +``` + +To use an `or` condition, pass to the filter object the `$or` property, whose value is an array of filters. + +In the example above, posts whose name is `My Post` or their `published_at` date is less than the current date and time are retrieved. + + # update Method - Service Factory Reference This method updates one or more records of the data model. @@ -33834,92 +33763,163 @@ Learn more about accepted filters in [this documentation](https://docs.medusajs. The method returns an array of objects of updated records. -# restore Method - Service Factory Reference +# delete 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). +This method deletes one or more records. -## Restore One Record +## Delete One Record ```ts -const restoredPosts = await postModuleService.restorePosts("123") +await postModuleService.deletePosts("123") ``` -### Parameters - -To restore one record, pass its ID as a parameter of the method. - -### Returns - -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. - -For example, the returned object of the above example is: - -```ts -restoredPosts = { - post_id: ["123"], -} -``` +To delete one record, pass its ID as a parameter of the method. *** -## Restore Multiple Records +## Delete Multiple Records ```ts -const restoredPosts = await postModuleService.restorePosts([ +await postModuleService.deletePosts([ "123", "321", ]) ``` -### Parameters - -To restore multiple records, pass an array of IDs as a parameter of the method. - -### Returns - -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. - -For example, the returned object of the above example is: - -```ts -restoredPosts = { - post_id: [ - "123", - "321", - ], -} -``` +To delete multiple records, pass an array of IDs as a parameter of the method. *** -## Restore Records Matching Filters +## Delete Records Matching Filters ```ts -const restoredPosts = await postModuleService.restorePosts({ +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). + + +# 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 restore records matching a set of filters, pass an object of fitlers as a parameter of the method. +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 object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. +The method returns an array of the first `15` records matching the filters. -For example, the returned object of the above example is: +*** + +## 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 -restoredPosts = { - post_id: [ - "123", - ], -} +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. + diff --git a/www/apps/resources/app/plugins/guides/wishlist/page.mdx b/www/apps/resources/app/plugins/guides/wishlist/page.mdx index edeeada571..6c0c4f95b6 100644 --- a/www/apps/resources/app/plugins/guides/wishlist/page.mdx +++ b/www/apps/resources/app/plugins/guides/wishlist/page.mdx @@ -1418,7 +1418,7 @@ The response will contain a list of products. You can use the `id` of a product' Then, send a `POST` request to the `/store/customers/me/wishlists/items` API route to add the variant to the wishlist: ```bash -curl -X GET 'localhost:9000/store/customers/me/wishlists/items' \ +curl -X POST 'localhost:9000/store/customers/me/wishlists/items' \ --header 'Content-Type: application/json' \ --header 'x-publishable-api-key: {api_key}' \ --header 'Authorization: Bearer {token}' \ diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index 257755db0b..f00706b4e8 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -5855,7 +5855,7 @@ export const generatedEditDates = { "references/core_flows/types/core_flows.ThrowUnlessPaymentCollectionNotePaidInput/page.mdx": "2025-01-17T16:43:25.819Z", "references/core_flows/types/core_flows.ValidatePaymentsRefundStepInput/page.mdx": "2025-01-17T16:43:26.128Z", "references/core_flows/types/core_flows.ValidateRefundStepInput/page.mdx": "2025-03-04T13:33:47.462Z", - "app/plugins/guides/wishlist/page.mdx": "2025-03-11T08:56:19.626Z", + "app/plugins/guides/wishlist/page.mdx": "2025-03-19T06:30:47.430Z", "app/plugins/page.mdx": "2025-02-26T11:39:25.709Z", "app/admin-components/components/data-table/page.mdx": "2025-03-03T14:55:58.556Z", "references/order_models/variables/order_models.Order/page.mdx": "2025-01-27T11:43:58.788Z",