From c26ef84cae3992172b734cb6c68bd47aba642682 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Mon, 27 Jan 2025 11:32:53 +0200 Subject: [PATCH] docs: added plugins documentation (#10989) (Should be merged after the next release) Closes DX-1294 --- .../reuse-customizations/page.mdx | 17 + .../data-models/manage-relationships/page.mdx | 10 +- .../data-models/relationships/page.mdx | 14 +- .../fundamentals/modules/options/page.mdx | 28 +- .../app/learn/fundamentals/modules/page.mdx | 30 + .../fundamentals/plugins/create/page.mdx | 304 +++ .../app/learn/fundamentals/plugins/page.mdx | 62 + .../learn/introduction/architecture/page.mdx | 26 +- www/apps/book/generated/edit-dates.mjs | 29 +- www/apps/book/generated/sidebar.mjs | 36 +- www/apps/book/sidebar.mjs | 17 + .../resources/app/create-medusa-app/page.mdx | 31 +- .../app/medusa-cli/commands/exec/page.mdx | 2 +- .../app/medusa-cli/commands/plugin/page.mdx | 100 + .../commands/start-cluster/page.mdx | 2 +- .../app/medusa-cli/commands/telemtry/page.mdx | 2 +- .../app/plugins/guides/wishlist/page.mdx | 2112 +++++++++++++++++ www/apps/resources/app/plugins/page.mdx | 21 + .../restock-notification/page.mdx | 4 +- www/apps/resources/generated/edit-dates.mjs | 13 +- www/apps/resources/generated/files-map.mjs | 12 + www/apps/resources/generated/sidebar.mjs | 44 + www/apps/resources/sidebar.mjs | 8 + www/apps/resources/sidebars/plugins.mjs | 20 + .../components/WorkflowDiagram/List/index.tsx | 3 +- .../src/components/WorkflowDiagram/index.tsx | 6 +- 26 files changed, 2901 insertions(+), 52 deletions(-) create mode 100644 www/apps/book/app/learn/customization/reuse-customizations/page.mdx create mode 100644 www/apps/book/app/learn/fundamentals/plugins/create/page.mdx create mode 100644 www/apps/book/app/learn/fundamentals/plugins/page.mdx create mode 100644 www/apps/resources/app/medusa-cli/commands/plugin/page.mdx create mode 100644 www/apps/resources/app/plugins/guides/wishlist/page.mdx create mode 100644 www/apps/resources/app/plugins/page.mdx create mode 100644 www/apps/resources/sidebars/plugins.mjs diff --git a/www/apps/book/app/learn/customization/reuse-customizations/page.mdx b/www/apps/book/app/learn/customization/reuse-customizations/page.mdx new file mode 100644 index 0000000000000..9243cbbe79e96 --- /dev/null +++ b/www/apps/book/app/learn/customization/reuse-customizations/page.mdx @@ -0,0 +1,17 @@ +export const metadata = { + title: `${pageNumber} Re-Use Customizations with Plugins`, +} + +# {metadata.title} + +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](../../fundamentals/plugins/page.mdx). diff --git a/www/apps/book/app/learn/fundamentals/data-models/manage-relationships/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/manage-relationships/page.mdx index bba9dbe814b5e..3c5faf160044b 100644 --- a/www/apps/book/app/learn/fundamentals/data-models/manage-relationships/page.mdx +++ b/www/apps/book/app/learn/fundamentals/data-models/manage-relationships/page.mdx @@ -198,7 +198,7 @@ For example, assuming you have the [Order, Product, and OrderProduct models from class HelloModuleService extends MedusaService({ Order, Product, - OrderProduct + OrderProduct, }) {} ``` @@ -212,16 +212,16 @@ const orderProduct = await helloModuleService.createOrderProducts({ order_id: "123", product_id: "123", metadata: { - test: true - } + test: true, + }, }) // update order-product association const orderProduct = await helloModuleService.updateOrderProducts({ id: "123", metadata: { - test: false - } + test: false, + }, }) // delete order-product association diff --git a/www/apps/book/app/learn/fundamentals/data-models/relationships/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/relationships/page.mdx index 150da2901acc1..c154bd812ca54 100644 --- a/www/apps/book/app/learn/fundamentals/data-models/relationships/page.mdx +++ b/www/apps/book/app/learn/fundamentals/data-models/relationships/page.mdx @@ -217,29 +217,29 @@ export const manyToManyColumnHighlights = [ ] ```ts highlights={manyToManyColumnHighlights} -import { model } from "@medusajs/framework/utils"; +import { model } from "@medusajs/framework/utils" export const Order = model.define("order_test", { id: model.id().primaryKey(), products: model.manyToMany(() => Product, { - pivotEntity: () => OrderProduct - }) + pivotEntity: () => OrderProduct, + }), }) export const Product = model.define("product_test", { id: model.id().primaryKey(), - orders: model.manyToMany(() => Order) + orders: model.manyToMany(() => Order), }) export const OrderProduct = model.define("orders_products", { id: model.id().primaryKey(), order: model.belongsTo(() => Order, { - mappedBy: "products" + mappedBy: "products", }), product: model.belongsTo(() => Product, { - mappedBy: "orders" + mappedBy: "orders", }), - metadata: model.json().nullable() + metadata: model.json().nullable(), }) ``` diff --git a/www/apps/book/app/learn/fundamentals/modules/options/page.mdx b/www/apps/book/app/learn/fundamentals/modules/options/page.mdx index 7635e9095b336..19c3fd000cb60 100644 --- a/www/apps/book/app/learn/fundamentals/modules/options/page.mdx +++ b/www/apps/book/app/learn/fundamentals/modules/options/page.mdx @@ -8,9 +8,7 @@ In this chapter, you’ll learn about passing options to your module from the Me ## What are Module Options? -A module can receive options to customize or configure its functionality. - -For example, if you’re creating a module that integrates a third-party service, you’ll want to receive the integration credentials in the options rather than adding them directly in your code. +A module can receive options to customize or configure its functionality. For example, if you’re creating a module that integrates a third-party service, you’ll want to receive the integration credentials in the options rather than adding them directly in your code. --- @@ -20,7 +18,7 @@ To pass options to a module, add an `options` property to the module’s configu For example: -```js title="medusa-config.ts" +```ts title="medusa-config.ts" module.exports = defineConfig({ // ... modules: [ @@ -36,6 +34,28 @@ module.exports = defineConfig({ The `options` property’s value is an object. You can pass any properties you want. +### Pass Options to a Module in a Plugin + +If your module is part of a plugin, you can pass options to the module in the plugin’s configuration. + +For example: + +```ts title="medusa-config.ts" +import { defineConfig } from "@medusajs/framework/utils" +module.exports = defineConfig({ + plugins: [ + { + resolve: "@myorg/plugin-name", + options: { + capitalize: true, + }, + }, + ], +}) +``` + +The `options` property in the plugin configuration is passed to all modules in a plugin. + --- ## Access Module Options in Main Service diff --git a/www/apps/book/app/learn/fundamentals/modules/page.mdx b/www/apps/book/app/learn/fundamentals/modules/page.mdx index d0ffff922f777..b4e682e5b9e9e 100644 --- a/www/apps/book/app/learn/fundamentals/modules/page.mdx +++ b/www/apps/book/app/learn/fundamentals/modules/page.mdx @@ -16,6 +16,18 @@ Medusa removes this overhead by allowing you to easily write custom modules that As you learn more about Medusa, you will see that modules are central to customizations and integrations. With modules, your Medusa application can turn into a middleware solution for your commerce ecosystem. + + +- You want to build a custom feature related to a single domain or integrate a third-party service. + + + + + +- You want to create a reusable package of customizations that include not only modules, but also API routes, workflows, and other customizations. Instead, use a [plugin](../plugins/page.mdx). + + + --- ## How to Create a Module? @@ -136,6 +148,12 @@ You export `BLOG_MODULE` to reference the module's name more reliably when resol ### 4. Add Module to Medusa's Configurations + + +If you're creating the module in a plugin, this step isn't required as the module is registered when the plugin is registered. Learn more about plugins in [this documentation](../plugins/page.mdx). + + + Once you finish building the module, add it to Medusa's configurations to start using it. Medusa will then register the module's main service in the Medusa container, allowing you to resolve and use it in other customizations. In `medusa-config.ts`, add a `modules` property and pass an array with your custom module: @@ -165,6 +183,12 @@ You don't have to write migrations yourself. Medusa's CLI tool has a command tha 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](!resources!/medusa-cli/commands/plugin#plugindbgenerate) instead. + + + ```bash npx medusa db:generate blog ``` @@ -193,6 +217,12 @@ In the migration class, the `up` method creates the table `post` and defines its 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 ``` diff --git a/www/apps/book/app/learn/fundamentals/plugins/create/page.mdx b/www/apps/book/app/learn/fundamentals/plugins/create/page.mdx new file mode 100644 index 0000000000000..0363a92582c33 --- /dev/null +++ b/www/apps/book/app/learn/fundamentals/plugins/create/page.mdx @@ -0,0 +1,304 @@ +import { Prerequisites, CardList } from "docs-ui" + +export const metadata = { + title: `${pageNumber} Create a Plugin`, +} + +# {metadata.title} + +In this chapter, you'll learn how to create a Medusa plugin and publish it. + +A [plugin](../page.mdx) is a package of reusable Medusa customizations that you can install in any Medusa application. By creating and publishing a plugin, you can reuse your Medusa customizations across multiple projects or share them with the community. + + + +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + + + +## 1. Create a Plugin Project + +Plugins are created in a separate Medusa project. This makes the development and publishing of the plugin easier. Later, you'll install that plugin in your Medusa application to test it out and use it. + +Medusa's `create-medusa-app` CLI tool provides the option to create a plugin project. Run the following command to create a new plugin project: + +```bash +npx create-medusa-app my-plugin --plugin +``` + +This will create a new Medusa plugin project in the `my-plugin` directory. + +### Plugin Directory Structure + +After the installation is done, the plugin structure will look like this: + +![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) + +- `src/`: Contains the Medusa customizations. +- `src/admin`: Contains [admin extensions](../../admin/page.mdx). +- `src/api`: Contains [API routes](../../api-routes/page.mdx) and [middlewares](../../api-routes/middlewares/page.mdx). You can add store, admin, or any custom API routes. +- `src/jobs`: Contains [scheduled jobs](../../scheduled-jobs/page.mdx). +- `src/links`: Contains [module links](../../module-links/page.mdx). +- `src/modules`: Contains [modules](../../modules/page.mdx). +- `src/subscribers`: Contains [subscribers](../../events-and-subscribers/page.mdx). +- `src/workflows`: Contains [workflows](../../workflows/page.mdx). You can also add [hooks](../../workflows/add-workflow-hook/page.mdx) under `src/workflows/hooks`. +- `package.json`: Contains the plugin's package information, including general information and dependencies. +- `tsconfig.json`: Contains the TypeScript configuration for the plugin. + +--- + +## 2. Prepare Plugin + +Before developing, testing, and publishing your plugin, make sure its name in `package.json` is correct. This is the name you'll use to install the plugin in your Medusa application. + +For example: + +```json title="package.json" +{ + "name": "@myorg/plugin-name", + // ... +} +``` + +--- + +## 3. Publish Plugin Locally for Development and Testing + +Medusa's CLI tool provides commands to simplify developing and testing your plugin in a local Medusa application. You start by publishing your plugin in the local package registry, then install it in your Medusa application. You can then watch for changes in the plugin as you develop it. + +### Publish and Install Local Package + + + +The first time you create your plugin, you need to publish the package into a local package registry, then install it in your Medusa application. This is a one-time only process. + +To publish the plugin to the local registry, run the following command in your plugin project: + +```bash title="Plugin project" +npx medusa plugin:publish +``` + +This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. + +Next, navigate to your Medusa application: + +```bash title="Medusa application" +cd ~/path/to/medusa-app +``` + +Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. + +Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: + +```bash npm2yarn title="Medusa application" +npm install --save-dev yalc +``` + +After that, run the following Medusa CLI command to install the plugin: + +```bash title="Medusa application" +npx medusa plugin:add @myorg/plugin-name +``` + +Make sure to replace `@myorg/plugin-name` with the name of your plugin as specified in `package.json`. Your plugin will be installed from the local package registry into your Medusa application. + +### Register Plugin in Medusa Application + +After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. + +Add the plugin to the `plugins` array in the `medusa-config.ts` file: + +export const pluginHighlights = [ + ["5", `"@myorg/plugin-name"`, "Replace with your plugin name."], +] + +```ts title="medusa-config.ts" highlights={pluginHighlights} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: {}, + }, + ], +}) +``` + +The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package. + +#### Pass Module Options through Plugin + +Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. + +For example: + +export const pluginOptionsHighlight = [ + ["6", "options", "Options to pass to the plugin's modules."] +] + +```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: { + apiKey: true, + }, + }, + ], +}) +``` + +The `options` property in the plugin configuration is passed to all modules in the plugin. Learn more about module options in [this chapter](../../modules/options/page.mdx). + +### Watch Plugin Changes During Development + +While developing your plugin, you can watch for changes in the plugin and automatically update the plugin in the Medusa application using it. This is the only command you'll continuously need during your plugin development. + +To do that, run the following command in your plugin project: + +```bash title="Plugin project" +npx medusa plugin:develop +``` + +This command will: + +- Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built. +- Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions. + +### Start Medusa Application + +You can start your Medusa application's development server to test out your plugin: + +```bash npm2yarn title="Medusa application" +npm run dev +``` + +While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. + +--- + +## 4. Create Customizations in the Plugin + +You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. + + + +While building those customizations, you can test them in your Medusa application by [watching the plugin changes](#watch-plugin-changes-during-development) and [starting the Medusa application](#start-medusa-application). + +### Generating Migrations for Modules + +During your development, you may need to generate migrations for modules in your plugin. To do that, use the `plugin:db:generate` command: + +```bash title="Plugin project" +npx medusa plugin:db:generate +``` + +This command generates migrations for all modules in the plugin. You can then run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command: + +```bash title="Medusa application" +npx medusa db:migrate +``` + +--- + +## 5. Publish Plugin to NPM + +Medusa's CLI tool provides a command that bundles your plugin to be published to npm. Once you're ready to publish your plugin publicly, run the following command in your plugin project: + +```bash +npx medusa plugin:build +``` + +The command will compile an output in the `.medusa/server` directory. + +You can now publish the plugin to npm using the [NPM CLI tool](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Run the following command to publish the plugin to npm: + +```bash +npm publish +``` + +If you haven't logged in before with your NPM account, you'll be asked to log in first. Then, your package is published publicly to be used in any Medusa application. + +### Install Public Plugin in Medusa Application + +You install a plugin that's published publicly using your package manager. For example: + +```bash npm2yarn +npm install @myorg/plugin-name +``` + +Where `@myorg/plugin-name` is the name of your plugin as published on NPM. + +Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). + +--- + +## Update a Published Plugin + +If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. + +First, run the following command to change the version of the plugin: + +```bash +npm version +``` + +Where `` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. Refer to the [npm version documentation](https://docs.npmjs.com/cli/v10/commands/npm-version) for more information. + +Then, re-run the same commands for publishing a plugin: + +```bash +npx medusa plugin:build +npm publish +``` + +This will publish an updated version of your plugin under a new version. diff --git a/www/apps/book/app/learn/fundamentals/plugins/page.mdx b/www/apps/book/app/learn/fundamentals/plugins/page.mdx new file mode 100644 index 0000000000000..1c6a72ab09f4f --- /dev/null +++ b/www/apps/book/app/learn/fundamentals/plugins/page.mdx @@ -0,0 +1,62 @@ +export const metadata = { + title: `${pageNumber} Plugins`, +} + +# {metadata.title} + +In this chapter, you'll learn what a plugin is in Medusa. + + + +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + + + +## What is a Plugin? + +A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](../modules/page.mdx), [API Routes](../api-routes/page.mdx), [Workflows](../workflows/page.mdx), [Workflow Hooks](../workflows/workflow-hooks/page.mdx), [Links](../module-links/page.mdx), [Subscribers](../events-and-subscribers/page.mdx), [Scheduled Jobs](../scheduled-jobs/page.mdx), and [Admin Extensions](../admin/page.mdx). + +Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project. + +![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg) + + + +Learn how to create a wishlist plugin in [this guide](!resources!/plugins/guides/wishlist). + + + +--- + +## Plugin vs Module + +A [module](../modules/page.mdx) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase. + +A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it. + +For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service. + + + +- You want to reuse your Medusa customizations across multiple projects. +- You want to share your Medusa customizations with the community. + + + + + +- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](../modules/page.mdx). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow. + + + +--- + +## How to Create a Plugin? + +The next chapter explains how you can create and publish a plugin. + +--- + +## Plugin Guides and Resources + +For more resources and guides related to plugins, refer to the [Resources documentation](!resources!/plugins). diff --git a/www/apps/book/app/learn/introduction/architecture/page.mdx b/www/apps/book/app/learn/introduction/architecture/page.mdx index e754f9daca967..4a9b910387a50 100644 --- a/www/apps/book/app/learn/introduction/architecture/page.mdx +++ b/www/apps/book/app/learn/introduction/architecture/page.mdx @@ -17,15 +17,25 @@ In a common Medusa application, requests go through four layers in the stack. In 3. Modules: Workflows use domain-specific modules for resource management. 3. Data store: Modules query the underlying datastore, which is a PostgreSQL database in common cases. + + +These layers of stack can be implemented within [plugins](../../fundamentals/plugins/page.mdx). + + + ![Diagram illustrating the HTTP layer](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) --- ## Database Layer -The Medusa application injects into each module a connection to the configured PostgreSQL database. +The Medusa application injects into each module a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. + + -Modules use that connection to read and write data to the database. +Modules can be implemented within [plugins](../../fundamentals/plugins/page.mdx). + + ![Diagram illustrating the database layer](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) @@ -33,19 +43,23 @@ Modules use that connection to read and write data to the database. ## Service Integrations -Third-party services are integrated through commerce and architectural modules. +Third-party services are integrated through commerce and architectural modules. You also create custom third-party integrations through a custom module. + + + +Modules can be implemented within [plugins](../../fundamentals/plugins/page.mdx). -You also create custom third-party integrations through a custom module. + ### Commerce Modules -Commerce modules integrate third-party services relevant for commerce or user-facing features. For example, you integrate Stripe through a payment module provider. +[Commerce modules](!resources!/commerce-modules) integrate third-party services relevant for commerce or user-facing features. For example, you integrate Stripe through a payment module provider. ![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 integrate third-party services and systems for architectural features. For example, you integrate Redis as a pub/sub service to send events, or SendGrid to send notifications. +[Architectural modules](!resources!/architectural-modules) integrate third-party services and systems for architectural features. For example, you integrate Redis as a pub/sub service to send events, or SendGrid to send notifications. ![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) diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index 57a327fe37f0a..b2618da3c15ba 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -32,15 +32,15 @@ export const generatedEditDates = { "app/learn/fundamentals/data-models/default-properties/page.mdx": "2024-10-21T13:30:21.368Z", "app/learn/fundamentals/workflows/advanced-example/page.mdx": "2024-09-11T10:46:59.975Z", "app/learn/fundamentals/events-and-subscribers/emit-event/page.mdx": "2024-11-25T16:19:32.168Z", - "app/learn/fundamentals/workflows/conditions/page.mdx": "2024-12-11T08:44:00.239Z", + "app/learn/fundamentals/workflows/conditions/page.mdx": "2025-01-27T08:45:19.027Z", "app/learn/fundamentals/modules/module-link-directions/page.mdx": "2024-07-24T09:16:01+02:00", "app/learn/fundamentals/admin/page.mdx": "2024-10-23T07:08:55.898Z", - "app/learn/fundamentals/workflows/long-running-workflow/page.mdx": "2024-12-04T07:37:59.822Z", + "app/learn/fundamentals/workflows/long-running-workflow/page.mdx": "2025-01-27T08:45:19.028Z", "app/learn/fundamentals/workflows/constructor-constraints/page.mdx": "2024-12-12T15:09:41.179Z", "app/learn/fundamentals/data-models/write-migration/page.mdx": "2024-11-11T15:27:59.794Z", "app/learn/fundamentals/data-models/manage-relationships/page.mdx": "2024-12-12T14:23:26.254Z", "app/learn/fundamentals/modules/remote-query/page.mdx": "2024-07-21T21:20:24+02:00", - "app/learn/fundamentals/modules/options/page.mdx": "2024-11-19T16:37:47.253Z", + "app/learn/fundamentals/modules/options/page.mdx": "2025-01-16T09:21:38.244Z", "app/learn/fundamentals/data-models/relationships/page.mdx": "2024-12-12T14:08:50.686Z", "app/learn/fundamentals/workflows/compensation-function/page.mdx": "2024-12-06T14:34:50.384Z", "app/learn/fundamentals/modules/service-factory/page.mdx": "2024-10-21T13:30:21.371Z", @@ -50,7 +50,7 @@ export const generatedEditDates = { "app/learn/fundamentals/scheduled-jobs/execution-number/page.mdx": "2024-10-21T13:30:21.371Z", "app/learn/fundamentals/api-routes/parameters/page.mdx": "2024-11-19T16:37:47.251Z", "app/learn/fundamentals/api-routes/http-methods/page.mdx": "2024-10-21T13:30:21.367Z", - "app/learn/fundamentals/admin/tips/page.mdx": "2024-12-12T11:43:26.003Z", + "app/learn/fundamentals/admin/tips/page.mdx": "2025-01-27T08:45:19.023Z", "app/learn/fundamentals/api-routes/cors/page.mdx": "2024-12-09T13:04:04.357Z", "app/learn/fundamentals/admin/ui-routes/page.mdx": "2024-12-09T16:44:40.198Z", "app/learn/fundamentals/api-routes/middlewares/page.mdx": "2024-12-09T13:04:03.712Z", @@ -78,18 +78,18 @@ export const generatedEditDates = { "app/learn/fundamentals/module-links/query/page.mdx": "2024-12-09T15:54:44.798Z", "app/learn/fundamentals/modules/db-operations/page.mdx": "2024-12-09T14:40:50.581Z", "app/learn/fundamentals/modules/multiple-services/page.mdx": "2024-10-21T13:30:21.370Z", - "app/learn/fundamentals/modules/page.mdx": "2024-12-09T15:55:25.858Z", + "app/learn/fundamentals/modules/page.mdx": "2025-01-16T08:34:28.947Z", "app/learn/debugging-and-testing/instrumentation/page.mdx": "2024-12-09T15:33:05.121Z", - "app/learn/fundamentals/api-routes/additional-data/page.mdx": "2025-01-23T15:54:44.613Z", - "app/learn/fundamentals/workflows/variable-manipulation/page.mdx": "2024-12-09T15:57:54.506Z", + "app/learn/fundamentals/api-routes/additional-data/page.mdx": "2025-01-27T08:45:19.025Z", + "app/learn/fundamentals/workflows/variable-manipulation/page.mdx": "2025-01-27T08:45:19.029Z", "app/learn/customization/custom-features/api-route/page.mdx": "2024-12-09T10:39:30.046Z", "app/learn/customization/custom-features/module/page.mdx": "2024-12-09T14:36:02.100Z", "app/learn/customization/custom-features/workflow/page.mdx": "2024-12-09T14:36:29.482Z", "app/learn/customization/extend-features/extend-create-product/page.mdx": "2025-01-06T11:18:58.250Z", "app/learn/customization/custom-features/page.mdx": "2024-12-09T10:46:28.593Z", "app/learn/customization/customize-admin/page.mdx": "2024-12-09T11:02:38.801Z", - "app/learn/customization/customize-admin/route/page.mdx": "2025-01-22T16:23:31.772Z", - "app/learn/customization/customize-admin/widget/page.mdx": "2024-12-09T11:02:39.108Z", + "app/learn/customization/customize-admin/route/page.mdx": "2025-01-27T08:45:19.021Z", + "app/learn/customization/customize-admin/widget/page.mdx": "2025-01-27T08:45:19.022Z", "app/learn/customization/extend-features/define-link/page.mdx": "2024-12-09T11:02:39.346Z", "app/learn/customization/extend-features/page.mdx": "2024-12-09T11:02:39.244Z", "app/learn/customization/extend-features/query-linked-records/page.mdx": "2024-12-09T11:02:39.519Z", @@ -99,16 +99,19 @@ export const generatedEditDates = { "app/learn/customization/integrate-systems/service/page.mdx": "2024-12-09T11:02:39.594Z", "app/learn/customization/next-steps/page.mdx": "2024-12-06T14:34:53.356Z", "app/learn/fundamentals/modules/architectural-modules/page.mdx": "2024-10-21T13:30:21.367Z", - "app/learn/introduction/architecture/page.mdx": "2024-10-21T13:30:21.368Z", + "app/learn/introduction/architecture/page.mdx": "2025-01-16T10:25:10.780Z", "app/learn/fundamentals/data-models/infer-type/page.mdx": "2024-12-09T15:54:08.713Z", "app/learn/fundamentals/custom-cli-scripts/seed-data/page.mdx": "2024-12-09T14:38:06.385Z", "app/learn/fundamentals/environment-variables/page.mdx": "2024-12-09T11:00:57.428Z", "app/learn/build/page.mdx": "2024-12-09T11:05:17.383Z", "app/learn/deployment/general/page.mdx": "2024-11-25T14:33:50.439Z", "app/learn/fundamentals/workflows/multiple-step-usage/page.mdx": "2024-11-25T16:19:32.169Z", - "app/learn/installation/page.mdx": "2025-01-06T09:12:48.690Z", + "app/learn/installation/page.mdx": "2025-01-27T08:45:19.029Z", "app/learn/fundamentals/data-models/check-constraints/page.mdx": "2024-12-06T14:34:50.384Z", "app/learn/fundamentals/module-links/link/page.mdx": "2025-01-06T09:27:25.604Z", - "app/learn/fundamentals/workflows/store-executions/page.mdx": "2025-01-24T12:09:24.087Z", - "app/learn/update/page.mdx": "2025-01-24T17:35:21.335Z" + "app/learn/fundamentals/plugins/create/page.mdx": "2025-01-22T10:14:47.933Z", + "app/learn/fundamentals/plugins/page.mdx": "2025-01-22T10:14:10.433Z", + "app/learn/customization/reuse-customizations/page.mdx": "2025-01-22T10:01:57.665Z", + "app/learn/fundamentals/workflows/store-executions/page.mdx": "2025-01-27T08:45:19.028Z", + "app/learn/update/page.mdx": "2025-01-27T08:45:19.030Z" } \ No newline at end of file diff --git a/www/apps/book/generated/sidebar.mjs b/www/apps/book/generated/sidebar.mjs index 5cb8fae62e0d1..5a02aa0b79200 100644 --- a/www/apps/book/generated/sidebar.mjs +++ b/www/apps/book/generated/sidebar.mjs @@ -181,6 +181,15 @@ export const generatedSidebar = [ ], "chapterTitle": "2.4. Integrate Systems" }, + { + "loaded": true, + "isPathHref": true, + "type": "link", + "title": "Re-Use Customizations", + "path": "/learn/customization/reuse-customizations", + "children": [], + "chapterTitle": "2.5. Re-Use Customizations" + }, { "loaded": true, "isPathHref": true, @@ -188,7 +197,7 @@ export const generatedSidebar = [ "title": "Next Steps", "path": "/learn/customization/next-steps", "children": [], - "chapterTitle": "2.5. Next Steps" + "chapterTitle": "2.6. Next Steps" } ], "chapterTitle": "2. Customize" @@ -792,6 +801,25 @@ export const generatedSidebar = [ ], "chapterTitle": "3.9. Admin Development" }, + { + "loaded": true, + "isPathHref": true, + "type": "link", + "path": "/learn/fundamentals/plugins", + "title": "Plugins", + "children": [ + { + "loaded": true, + "isPathHref": true, + "type": "link", + "path": "/learn/fundamentals/plugins/create", + "title": "Create Plugin", + "children": [], + "chapterTitle": "3.10.1. Create Plugin" + } + ], + "chapterTitle": "3.10. Plugins" + }, { "loaded": true, "isPathHref": true, @@ -806,10 +834,10 @@ export const generatedSidebar = [ "path": "/learn/fundamentals/custom-cli-scripts/seed-data", "title": "Seed Data", "children": [], - "chapterTitle": "3.10.1. Seed Data" + "chapterTitle": "3.11.1. Seed Data" } ], - "chapterTitle": "3.10. Custom CLI Scripts" + "chapterTitle": "3.11. Custom CLI Scripts" }, { "loaded": true, @@ -818,7 +846,7 @@ export const generatedSidebar = [ "title": "Environment Variables", "path": "/learn/fundamentals/environment-variables", "children": [], - "chapterTitle": "3.11. Environment Variables" + "chapterTitle": "3.12. Environment Variables" } ], "chapterTitle": "3. Fundamentals" diff --git a/www/apps/book/sidebar.mjs b/www/apps/book/sidebar.mjs index 2a244615d612c..48aa9815c96a8 100644 --- a/www/apps/book/sidebar.mjs +++ b/www/apps/book/sidebar.mjs @@ -106,6 +106,11 @@ export const sidebar = sidebarAttachHrefCommonOptions([ }, ], }, + { + type: "link", + title: "Re-Use Customizations", + path: "/learn/customization/reuse-customizations", + }, { type: "link", title: "Next Steps", @@ -458,6 +463,18 @@ export const sidebar = sidebarAttachHrefCommonOptions([ }, ], }, + { + type: "link", + path: "/learn/fundamentals/plugins", + title: "Plugins", + children: [ + { + type: "link", + path: "/learn/fundamentals/plugins/create", + title: "Create Plugin", + }, + ], + }, { type: "link", path: "/learn/fundamentals/custom-cli-scripts", diff --git a/www/apps/resources/app/create-medusa-app/page.mdx b/www/apps/resources/app/create-medusa-app/page.mdx index 58b1720e014c8..628bf4f789fed 100644 --- a/www/apps/resources/app/create-medusa-app/page.mdx +++ b/www/apps/resources/app/create-medusa-app/page.mdx @@ -15,7 +15,7 @@ export const metadata = { # {metadata.title} -The `create-medusa-app` CLI tool simplifies the process of creating a new Medusa project and provides an onboarding experience. +The `create-medusa-app` CLI tool simplifies the process of creating a new Medusa project. It also allows you to setup a [Medusa plugin project](#create-a-medusa-plugin-project). + + + + `--plugin` + + + + + Create a [plugin project](#create-a-medusa-plugin-project) instead of a Medusa application. This option is available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + + + + + `false` + + + @@ -241,6 +258,18 @@ If the database already has the necessary migrations and you don't need the comm --- +## Create a Medusa Plugin Project + +The `create-medusa-app` tool can also be used to create a Medusa Plugin Project. You can do that by passing the `--plugin` option: + +```bash +npx create-medusa-app@latest my-plugin --plugin +``` + +Learn more about how to create a plugin in [this documentation](!docs!/learn/fundamentals/plugins). + +--- + ## Troubleshooting + +These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + + + +## plugin\:publish + +Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin\:add](#pluginadd) command. + +```bash +npx medusa plugin:publish +``` + +--- + +## plugin\:add + +Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin\:publish](#pluginpublish) command. + +```bash +npx medusa plugin:add [names...] +``` + +### Arguments + + + + + Argument + Description + Required + + + + + + + `names` + + + + + The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its `package.json` file. + + + + + Yes + + + + +
+ +--- + +## plugin\:develop + +Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry. + +```bash +npx medusa plugin:develop +``` + +--- + +## plugin\:db\:generate + +Generate migrations for all modules in a plugin. + +```bash +npx medusa plugin:db:generate +``` + +--- + +## plugin\:build + +Build a plugin before publishing it to NPM. The command will compile an output in the `.medusa/server` directory. + +```bash +npx medusa plugin:build +``` + diff --git a/www/apps/resources/app/medusa-cli/commands/start-cluster/page.mdx b/www/apps/resources/app/medusa-cli/commands/start-cluster/page.mdx index 8b4b49a313401..5f2dede626458 100644 --- a/www/apps/resources/app/medusa-cli/commands/start-cluster/page.mdx +++ b/www/apps/resources/app/medusa-cli/commands/start-cluster/page.mdx @@ -1,6 +1,6 @@ --- sidebar_label: "start-cluster" -sidebar_position: 7 +sidebar_position: 8 --- import { Table } from "docs-ui" diff --git a/www/apps/resources/app/medusa-cli/commands/telemtry/page.mdx b/www/apps/resources/app/medusa-cli/commands/telemtry/page.mdx index 5c41eb586f500..ce466c9129fea 100644 --- a/www/apps/resources/app/medusa-cli/commands/telemtry/page.mdx +++ b/www/apps/resources/app/medusa-cli/commands/telemtry/page.mdx @@ -1,6 +1,6 @@ --- sidebar_label: "telemetry" -sidebar_position: 8 +sidebar_position: 9 --- import { Table } from "docs-ui" diff --git a/www/apps/resources/app/plugins/guides/wishlist/page.mdx b/www/apps/resources/app/plugins/guides/wishlist/page.mdx new file mode 100644 index 0000000000000..d338f68ea2b3c --- /dev/null +++ b/www/apps/resources/app/plugins/guides/wishlist/page.mdx @@ -0,0 +1,2112 @@ +import { Github, PlaySolid } from "@medusajs/icons" +import { Prerequisites, CardList, WorkflowDiagram } from "docs-ui" + +export const ogImage = "https://res.cloudinary.com/dza7lstvk/image/upload/v1737564127/Medusa%20Resources/plugins-wishlist_ktut0d.jpg" + +export const metadata = { + title: `How to Build a Wishlist Plugin`, + openGraph: { + images: [ + { + url: ogImage, + width: 1600, + height: 836, + type: "image/jpeg" + } + ], + }, + twitter: { + images: [ + { + url: ogImage, + width: 1600, + height: 836, + type: "image/jpeg" + } + ] + } +} + +# {metadata.title} + +In this guide, you'll learn how to build a wishlist [plugin](!docs!/learn/fundamentals/plugins) in Medusa. + +When you install a Medusa application, you get a fully-fledged commerce platform with a framework for customization. The Medusa application's commerce features are built around [commerce modules](../../../commerce-modules/page.mdx) which are available out-of-the-box. + +Customers browsing your store may be interested in a product but not ready to buy it yet. They may want to save the product for later or share it with friends and family. A wishlist feature allows customers to save products they like and access them later. + +This guide will teach you how to: + +- Install and set up a Medusa application project. +- Install and set up a Medusa plugin. +- Implement the wishlist features in the plugin. + - Features include allowing customers to add products to a wishlist, view and manage their wishlist, and share their wishlist. +- Test and use the wishlist plugin in your Medusa application. + +You can follow this guide whether you're new to Medusa or an advanced Medusa developer. + + + +--- + +## Step 1: Install a Medusa Application + +You'll first install a Medusa application that exposes core commerce features through REST APIs. You'll later install the wishlist plugin in this application to test it out. + + + +Start by installing the Medusa application on your machine with the following command: + +```bash +npx create-medusa-app@latest +``` + +You'll be asked for the project's name. You can also optionally choose to install the [Next.js starter storefront](../../../nextjs-starter/page.mdx). + +Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name. + + + +The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](!docs!/learn/fundamentals/api-routes). Learn more about Medusa's architecture in [this documentation](!docs!/learn/introduction/architecture). + + + +Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard. + + + +Check out the [troubleshooting guides](../../../troubleshooting/create-medusa-app-errors/page.mdx) for help. + + + +--- + +## Step 2: Install a Medusa Plugin Project + +A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. You can add in the plugin [API Routes](!docs!/learn/fundamentals/api-routes), [Workflows](!docs!/learn/fundamentals/workflows), and other customizations, as you'll see in this guide. Afterward, you can test it out locally in a Medusa application, then publish it to npm to install and use it in any Medusa application. + + + +Learn more about plugins in [this documentation](!docs!/learn/fundamentals/plugins). + + + +A Medusa plugin is set up in a different project, giving you the flexibility in building and publishing it, while providing you with the tools to test it out locally in a Medusa application. + +To create a new Medusa plugin project, run the following command in a directory different than that of the Medusa application: + +```bash npm2yarn +npx create-medusa-app@latest medusa-plugin-wishlist --plugin +``` + +Where `medusa-plugin-wishlist` is the name of the plugin's directory and the name set in the plugin's `package.json`. So, if you wish to publish it to NPM later under a different name, you can change it here in the command or later in `package.json`. + +Once the installation process is done, a new directory named `medusa-plugin-wishlist` will be created with the plugin project files. + +![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) + +--- + +## Step 3: Set up Plugin in Medusa Application + +Before you start your development, you'll set up the plugin in the Medusa application you installed in the first step. This will allow you to test the plugin during your development process. + +In the plugin's directory, run the following command to publish the plugin to the local package registry: + +```bash title="Plugin project" +npx medusa plugin:publish +``` + +This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. + +Next, you'll install the plugin in the Medusa application from the local registry. + + + +If you've installed your Medusa project before v2.3.1, you must install [yalc](https://github.com/wclr/yalc) as a development dependency first. + + + +Run the following command in the Medusa application's directory to install the plugin: + +```bash title="Medusa application" +npx medusa plugin:add medusa-plugin-wishlist +``` + +This command installs the plugin in the Medusa application from the local package registry. + +Next, register the plugin in the `medusa-config.ts` file of the Medusa application: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "medusa-plugin-wishlist", + options: {}, + }, + ], +}) +``` + +Finally, to ensure your plugin's changes are constantly published to the local registry, simplifying your testing process, keep the following command running in the plugin project during development: + +```bash title="Plugin project" +npx medusa plugin:develop +``` + +--- + +## Step 4: Implement Wishlist Module + +To add custom tables to the database, which are called data models, you create a module. A module is a package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. + +While you can create modules outside of a plugin and install them in the Medusa application, plugins allow you to bundle modules with other customizations, such as API routes and workflows. + +In this step, you'll create a Wishlist Module within the wishlist plugin. This module adds custom data models for wishlists and their items, which you'll use in later steps to store a customer's wishlist. + + + +Learn more about modules in [this documentation](!docs!/learn/fundamentals/modules). + + + +### Create Module Directory + +A module is created under the `src/modules` directory of your plugin. So, create the directory `src/modules/wishlist`. + +![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1737461182/Medusa%20Resources/wishlist-1_z3kzfv.jpg) + +### Create Data Models + +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 documentation](!docs!/learn/fundamentals/modules#1-create-data-model). + + + +In the Wishlist Module, you'll create two data models: `Wishlist` and `WishlistItem`. The `Wishlist` model represents a customer's wishlist, while the `WishlistItem` model represents a product in the wishlist. + +Starting with the `Wishlist` model, create a file `src/modules/wishlist/models/wishlist.ts` with the following content: + +![Directory structure after adding the Wishlist model](https://res.cloudinary.com/dza7lstvk/image/upload/v1737461304/Medusa%20Resources/wishlist-2_co2lht.jpg) + +```ts title="src/modules/wishlist/models/wishlist.ts" +import { model } from "@medusajs/framework/utils" +import { WishlistItem } from "./wishlist-item" + +export const Wishlist = model.define("wishlist", { + id: model.id().primaryKey(), + customer_id: model.text(), + sales_channel_id: model.text(), + items: model.hasMany(() => WishlistItem), +}) +.indexes([ + { + on: ["customer_id", "sales_channel_id"], + unique: true, + }, +]) +``` + +The `Wishlist` model has the following properties: + +- `id`: A unique identifier for the wishlist. +- `customer_id`: The ID of the customer who owns the wishlist. +- `sales_channel_id`: The ID of the sales channel where the wishlist is created. In Medusa, product availability can differ between sales channels. This ensures only products available in the customer's sales channel are added to the wishlist. +- `items`: A relation to the `WishlistItem` model, representing the products in the wishlist. You'll add this data model next. + + + +Learn more about data model [properties](!docs!/learn/fundamentals/data-models/property-types) and [relations](!docs!/learn/fundamentals/data-models/relationships). + + + +You also define a unique index on the `customer_id` and `sales_channel_id` columns to ensure a customer can only have one wishlist per sales channel. + + + +Learn more about data model indexes in [this documentation](!docs!/learn/fundamentals/data-models/index). + + + +Next, create the `WishlistItem` model in the file `src/modules/wishlist/models/wishlist-item.ts`: + +![Directory structure after adding the WishlistItem model](https://res.cloudinary.com/dza7lstvk/image/upload/v1737461521/Medusa%20Resources/wishlist-3_fxcjxy.jpg) + +```ts title="src/modules/wishlist/models/wishlist-item.ts" +import { model } from "@medusajs/framework/utils" +import { Wishlist } from "./wishlist" + +export const WishlistItem = model.define("wishlist_item", { + id: model.id().primaryKey(), + product_variant_id: model.text(), + wishlist: model.belongsTo(() => Wishlist, { + mappedBy: "items", + }), +}) +.indexes([ + { + on: ["product_variant_id", "wishlist_id"], + unique: true, + }, +]) +``` + +The `WishlistItem` model has the following properties: + +- `id`: A unique identifier for the wishlist item. +- `product_variant_id`: The ID of the product variant in the wishlist. +- `wishlist`: A relation to the `Wishlist` model, representing the wishlist the item belongs to. + +You also define a unique index on the `product_variant_id` and `wishlist_id` columns to ensure a product variant is added to the wishlist only once. The `wishlist_id` column is available as a by-product of the `belongsTo` relation. + +### Create Service + +You define data-management methods of your data models in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can perform database operations. + + + +Learn more about services in [this documentation](!docs!/learn/fundamentals/modules#2-create-service). + + + +In this section, you'll create the Wishlist Module's service that's used to manage wishlists and wishlist items. Create the file `src/modules/wishlist/service.ts` with the following content: + +![Directory structure after adding the service file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737461698/Medusa%20Resources/wishlist-4_j5ka26.jpg) + +```ts title="src/modules/wishlist/service.ts" +import { MedusaService } from "@medusajs/framework/utils" +import { Wishlist } from "./models/wishlist" +import { WishlistItem } from "./models/wishlist-item" + +export default class WishlistModuleService extends MedusaService({ + Wishlist, + WishlistItem, +}) {} +``` + +The `WishlistModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods. + +So, the `WishlistModuleService` class now has methods like `createWishlists` and `retrieveWishlist`. + + + +Find all methods generated by the `MedusaService` in [this reference](../../../service-factory-reference/page.mdx). + + + +You'll use this service in a later method to store and manage wishlists and wishlist items in other customizations. + +### Export Module Definition + +The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service. + +So, create the file `src/modules/wishlist/index.ts` with the following content: + +![Directory structure after adding the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737461829/Medusa%20Resources/wishlist-5_mb4tjf.jpg) + +```ts title="src/modules/wishlist/index.ts" +import WishlistModuleService from "./service" +import { Module } from "@medusajs/framework/utils" + +export const WISHLIST_MODULE = "wishlist" + +export default Module(WISHLIST_MODULE, { + service: WishlistModuleService, +}) +``` + +You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters: + +1. The module's name, which is `wishlist`. +2. An object with a required property `service` indicating the module's service. + +You'll later use the module's service to manage wishlists and wishlist items in other customizations. + +### Generate Migrations + +Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module. + + + +Learn more about migrations in [this documentation](!docs!/learn/fundamentals/modules#5-generate-migrations). + + + +Medusa's CLI tool generates the migrations for you. To generate a migration for the Wishlist Module, run the following command in the plugin project: + +```bash title="Plugin project" +npx medusa plugin:db:generate +``` + +You'll now have a `migrations` directory under `src/modules/wishlist` that holds the generated migration. + +Then, to reflect these migrations on the database of the Medusa application using this module, run the following command: + + + +Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry. + + + +```bash title="Medusa application" +npx medusa db:migrate +``` + +The tables of the Wishlist Module's data models are now created in the database. + +--- + +## Step 5: Link Wishlist Data Models with Core Models + +The Wishlist Module's data models store IDs of records in data models implemented in Medusa's core commerce modules, such as the ID of a customer or a product variant. + +However, modules are [isolated](!docs!/learn/fundamentals/modules/isolation) to ensure they're re-usable and don't have side effects when integrated into the Medusa application. So, to build associations between modules, you define [module links](!docs!/learn/fundamentals/module-links). A Module link associates two modules' data models while maintaining module isolation. + +In this section, you'll link the `Wishlist` data model to the [Customer Module](../../../commerce-modules/customer/page.mdx)'s `Customer` data model, and to the [Sales Channel](../../../commerce-modules/sales-channel/page.mdx) Module's `SalesChannel` data model. You'll also link the `WishlistItem` data model to the [Product Module](../../../commerce-modules/product/page.mdx)'s `ProductVariant` data model. + + + +Learn more about module links in [this documentation](!docs!/learn/fundamentals/module-links). + + + +To create the link between the `Wishlist` data model and the `Customer` data model, create the file `src/modules/wishlist/links/wishlist-customer.ts` with the following content: + +![Directory structure after adding the link file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737462649/Medusa%20Resources/wishlist-6_xqytog.jpg) + +```ts title="src/modules/wishlist/links/wishlist-customer.ts" +import { defineLink } from "@medusajs/framework/utils" +import WishlistModule from "../modules/wishlist" +import CustomerModule from "@medusajs/medusa/customer" + +export default defineLink( + { + ...WishlistModule.linkable.wishlist.id, + field: "customer_id", + }, + CustomerModule.linkable.customer.id, + { + readOnly: true, + } +) +``` + +You define a link using `defineLink` from the Modules SDK. It accepts three parameters: + +1. The first data model part of the link, which is the Wishlist Module's `wishlist` data model. A module has a special `linkable` property that contain link configurations for its data models. You also specify the field that points to the customer. +1. The second data model part of the link, which is the Customer Module's `customer` data model. +3. An object of configurations for the module link. By default, Medusa creates a table in the database to represent the link you define. However, in this guide, you only want this link to retrieve the customer associated with a wishlist. So, you enable `readOnly` telling Medusa not to create a table for this link. + +Next, to create the link between the `Wishlist` data model and the `SalesChannel` data model, create the file `src/modules/wishlist/links/wishlist-sales-channel.ts` with the following content: + +![Directory structure after adding the link file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737462829/Medusa%20Resources/wishlist-7_ddiwxy.jpg) + +```ts title="src/modules/wishlist/links/wishlist-sales-channel.ts" +import { defineLink } from "@medusajs/framework/utils" +import WishlistModule from "../modules/wishlist" +import SalesChannelModule from "@medusajs/medusa/sales-channel" + +export default defineLink( + { + ...WishlistModule.linkable.wishlist.id, + field: "sales_channel_id", + }, + SalesChannelModule.linkable.salesChannel, + { + readOnly: true, + } +) +``` + +You define a link between the `Wishlist` data model and the `SalesChannel` data model in the same way as the previous link. + +Finally, to create the link between the `WishlistItem` data model and the `ProductVariant` data model, create the file `src/modules/wishlist/links/wishlist-product.ts` with the following content: + +![Directory structure after adding the link file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737467010/Medusa%20Resources/wishlist-8_hgoaby.jpg) + +```ts title="src/modules/wishlist/links/wishlist-product.ts" +import { defineLink } from "@medusajs/framework/utils" +import WishlistModule from "../modules/wishlist" +import ProductModule from "@medusajs/medusa/product" + +export default defineLink( + { + ...WishlistModule.linkable.wishlistItem.id, + field: "product_variant_id", + }, + ProductModule.linkable.productVariant, + { + readOnly: true, + } +) +``` + +You define a link between the `WishlistItem` data model and the `ProductVariant` data model in the same way as the previous links. + +In the next steps, you'll see how these links allow you to retrieve the resources associated with a wishlist or wishlist item. + +--- + +## Step 6: Create Wishlist Workflow + +The first feature you'll add to the wishlist plugin is the ability to create a wishlist for a customer. You'll implement this feature in a workflow. + +A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint. + +In this section, you'll create a workflow that creates a wishlist for a customer. Later, you'll execute this workflow from an API route. + + + +Learn more about workflows in [this documentation](!docs!/learn/fundamentals/workflows) + + + +The workflow has the following steps: + + + +You'll implement the steps before implementing the workflow. + +### validateCustomerCreateWishlistStep + +The first step in the workflow will validate that a customer doesn't have an existing workflow. If not valid, the step will throw an error, stopping the workflow's execution. + +To create the step, create the file `src/workflows/steps/validate-customer-create-wishlist.ts` with the following content: + +![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737467678/Medusa%20Resources/wishlist-9_lzlifn.jpg) + +export const validateCustomerWishlistHighlights = [ + ["13", "graph", "Retrieve the wishlist based on the specified customer ID."], + ["21", "", "Throw an error if the customer already has a wishlist."], + ["29", "graph", "Retrieve the customer based on the specified customer ID."], + ["28", "", "Throw an error if the customer doesn't exist."], +] + +```ts title="src/workflows/steps/validate-customer-create-wishlist.ts" highlights={validateCustomerWishlistHighlights} +import { MedusaError } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" + +type ValidateCustomerCreateWishlistStepInput = { + customer_id: string +} + +export const validateCustomerCreateWishlistStep = createStep( + "validate-customer-create-wishlist", + async ({ customer_id }: ValidateCustomerCreateWishlistStepInput, { container }) => { + const query = container.resolve("query") + + const { data } = await query.graph({ + entity: "wishlist", + fields: ["*"], + filters: { + customer_id: customer_id, + }, + }) + + if (data.length) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + "Customer already has a wishlist" + ) + } + + // check that customer exists + const { data: customers } = await query.graph({ + entity: "customer", + fields: ["*"], + filters: { + id: customer_id, + }, + }) + + if (customers.length === 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Specified customer was not found" + ) + } + } +) +``` + +You create a step using `createStep` from the Workflows SDK. It accepts two parameters: + +1. The step's name, which is `validate-customer-create-wishlist`. +1. An async function that executes the step's logic. The function receives two parameters: + - The input data for the step, which in this case is an object having a `customer_id` property. + - An object holding the workflow's context, including the [Medusa Container](!docs!learn/fundamentals/medusa-container) that allows you to resolve framework and commerce tools. + +In the step function, you use [Query](!docs!/learn/fundamentals/module-links/query) to retrieve the wishlist based on the specified customer ID. If a wishlist exists, you throw an error, stopping the workflow's execution. + +You also try to retrieve the customer, and if they don't exist, you throw an error. + +### createWishlistStep + +The second step in the workflow will create a wishlist for the customer. To create the step, create the file `src/workflows/steps/create-wishlist.ts` with the following content: + +![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737467998/Medusa%20Resources/wishlist-10_xex4d0.jpg) + +export const createWishlistStepHiglights = [ + ["13", "wishlistModuleService", "Resolve the Wishlist Module's service from the container."], + ["16", "createWishlists", "Create the wishlist."], + ["18", "wishlist", "Return the wishlist"], + ["18", "wishlist.id", "Pass the wishlist's ID to the compensation function."], + ["24", "deleteWishlists", "Delete the wishlist if an error occurs in the workflow."] +] + +```ts title="src/workflows/steps/create-wishlist.ts" highlights={createWishlistStepHiglights} +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { WISHLIST_MODULE } from "../../modules/wishlist" +import WishlistModuleService from "../../modules/wishlist/service" + +type CreateWishlistStepInput = { + customer_id: string + sales_channel_id: string +} + +export const createWishlistStep = createStep( + "create-wishlist", + async (input: CreateWishlistStepInput, { container }) => { + const wishlistModuleService: WishlistModuleService = + container.resolve(WISHLIST_MODULE) + + const wishlist = await wishlistModuleService.createWishlists(input) + + return new StepResponse(wishlist, wishlist.id) + }, + async (id, { container }) => { + const wishlistModuleService: WishlistModuleService = + container.resolve(WISHLIST_MODULE) + + await wishlistModuleService.deleteWishlists(id) + } +) +``` + +This step accepts the IDs of the customer and the sales channel as input. In the step function, you resolve the Wishlist Module's service from the container and use its generated `createWishlists` method to create the wishlist, passing it the input as a parameter. + +Steps that return data must return them in a `StepResponse` instance. The `StepResponse` constructor accepts two parameters: + +- The data to return, which in this case is the created wishlist. +- The data to pass to the compensation function, which in this case is the wishlist's ID. + +The compensation function is an optional third parameter of `createStep`. It defines rollback logic that's executed when an error occurs during the workflow's execution. In the compensation function, you undo the actions you performed in the step function. + +The compensation function accepts as a first parameter the data passed as a second parameter to the `StepResponse` returned by the step function, which in this case is the wishlist's ID. In the compensation function, you resolve the Wishlist Module's service from the container and use its generated `deleteWishlists` method to delete the wishlist. + + + +Learn more about the generated [create](../../../service-factory-reference/methods/create/page.mdx) and [delete](../../../service-factory-reference/methods/delete/page.mdx) methods. + + + +### Add createWishlistWorkflow + +You can now add the `createWishlistWorkflow` to the plugin. Create the file `src/workflows/create-wishlist.ts` with the following content: + +![Directory structure after adding the workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737468333/Medusa%20Resources/wishlist-11_absftb.jpg) + +export const createWishlistWorkflowHighlights = [ + ["13", "validateCustomerCreateWishlistStep", "Validate that the customer doesn't already have a wishlist."], + ["17", "createWishlistStep", "Create the wishlist."], + ["20", "wishlist", "Return the wishlist."] +] + +```ts title="src/workflows/create-wishlist.ts" highlights={createWishlistWorkflowHighlights} +import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { validateCustomerCreateWishlistStep } from "./steps/validate-customer-create-wishlist" +import { createWishlistStep } from "./steps/create-wishlist" + +type CreateWishlistWorkflowInput = { + customer_id: string + sales_channel_id: string +} + +export const createWishlistWorkflow = createWorkflow( + "create-wishlist", + (input: CreateWishlistWorkflowInput) => { + validateCustomerCreateWishlistStep({ + customer_id: input.customer_id, + }) + + const wishlist = createWishlistStep(input) + + return new WorkflowResponse({ + wishlist, + }) + } +) +``` + +You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter. + +It accepts as a second parameter a constructor function, which is the workflow's implementation. In the workflow, you: + +- Execute the `validateCustomerCreateWishlistStep` step to validate that the customer doesn't have an existing wishlist. +- Execute the `createWishlistStep` step to create the wishlist. + + + +A workflow's constructor function has some constraints in implementation, which is why you need to use `transform` for variable manipulation. Learn more about these constraints in [this documentation](!docs!/learn/fundamentals/workflows/constructor-constraints). + + + +Workflows must return an instance of `WorkflowResponse`, passing as a parameter the data to return to the workflow's executor. The workflow returns an object having a `wishlist` property, which is the created wishlist. + +You'll execute this workflow in an API route in the next step. + +--- + +## Step 7: Create Wishlist API Route + +Now that you implemented the flow to create a wishlist for a customer, you'll create an API route that exposes this functionality. + +An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create a `POST` API route at the path `/store/customers/me/wishlists` that executes the workflow from the previous step. + + + +Learn more about API routes in [this documentation](!docs!/learn/fundamentals/api-routes). + + + +An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`. + +So, to create the `/store/customers/me/wishlists` API route, create the file `src/api/store/customers/me/wishlists/route.ts` with the following content: + +![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737468859/Medusa%20Resources/wishlist-12_gvvb9z.jpg) + +```ts title="src/api/store/customers/me/wishlists/route.ts" +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { createWishlistWorkflow } from "../../../../../workflows/create-wishlist" +import { MedusaError } from "@medusajs/framework/utils" + +export async function POST( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) { + if (!req.publishable_key_context?.sales_channel_ids.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "At least one sales channel ID is required to be associated with the publishable API key in the request header." + ) + } + const { result } = await createWishlistWorkflow(req.scope) + .run({ + input: { + customer_id: req.auth_context.actor_id, + sales_channel_id: req.publishable_key_context?.sales_channel_ids[0], + }, + }) + + res.json({ + wishlist: result.wishlist, + }) +} +``` + +Since you export a `POST` function in this file, you're exposing a `POST` API route at `/store/customers/me/wishlists`. The route handler function accepts two parameters: + +1. A request object with details and context about the request, such as authenticated customer details. +2. A response object to manipulate and send the response. + +API routes implemented under the `/store` path require passing a publishable API key in the header of the request. The publishable API key is created by an admin user and is associated with one or more sales channels. In the route handler function, you validate that the request has at least one sales channel ID associated with the publishable API key. You'll use that sales channel ID with the wishlist you're creating. + +Also, API routes implemented under the `/store/customers/me` path are only accessible by authenticated customers. You access the ID of the authenticated customer using the `auth_context.actor_id` property of the request object. + +In the route handler function, you execute the `createWishlistWorkflow`, passing the authenticated customer ID and the sales channel ID as input. The workflow returns an object having a `result` property, which is the data returned by the workflow. You return the created wishlist in the response. + +### Test API Route + +You'll now test that this API route defined in the plugin is working as expected using the Medusa application you installed in the first step. + + + +Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry. + + + +In the Medusa application's directory, run the following command to start the development server: + +```bash npm2yarn +npm run dev +``` + +### Retrieve Publishable API Key + +Before sending the request, you need to obtain a publishable API key. So, open the Medusa Admin at `http://localhost:9000/app` and log in with the user you created earlier. + +To access your application's API keys in the admin, go to Settings -> Publishable API Keys. You'll have an API key created by default, which is associated with the default sales channel. You can use this publishable API key in the request header. + +![In the admin, click on Publishable API key in the sidebar. A table will show your API keys and allow you to create one.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733230421/Medusa%20Resources/Screenshot_2024-12-03_at_2.53.07_PM_gau9jy.png) + +### Retrieve Authenticated Customer Token + +Then, you need an authentication token of a registered customer. To create a customer, first, send the following request to the Medusa application: + +```bash +curl -X POST 'http://localhost:9000/auth/customer/emailpass/register' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "email": "customer@gmail.com", + "password": "supersecret" +}' +``` + +This API route obtains a registration token for the specified email and password in the request body. + +Next, use that token to register the customer: + +```bash +curl -X POST 'http://localhost:9000/store/customers' \ +--header 'Content-Type: application/json' \ +-H 'x-publishable-api-key: {api_key}' \ +--header 'Authorization Bearer {token}' \ +--data-raw '{ + "email": "customer@gmail.com" +}' +``` + +Make sure to replace `{api_key}` with the publishable API key you copied from the settings, and `{token}` with the token received from the previous request. + +This will create a customer. You can now obtain the customer's authentication token by sending the following request: + +```bash +curl -X POST 'http://localhost:9000/auth/customer/emailpass' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "email": "customer@gmail.com", + "password": "supersecret" +}' +``` + +This API route will return an authentication token for the customer. You'll use this token in the header of the following requests. + +### Send Request to Create Wishlist + +Finally, send a `POST` request to the `/store/customers/me/wishlists` API route to create a wishlist for the authenticated customer: + +```bash +curl -X POST 'localhost:9000/store/customers/me/wishlists' \ +--header 'x-publishable-api-key: {api_key}' \ +--header 'Authorization: Bearer {token}' +``` + +Make sure to replace `{api_key}` with the publishable API key you copied from the settings, and `{token}` with the authenticated customer token. + +You'll receive in the response the created wishlist. + +--- + +## Step 8: Retrieve Wishlist API Route + +In this step, you'll add an API route to retrieve a customer's wishlist. You'll create a `GET` API route at the path `/store/customers/me/wishlists` that retrieves the wishlist of the authenticated customer. + +So, add to the `src/api/store/customers/me/wishlists/route.ts` the following: + +```ts title="src/api/store/customers/me/wishlists/route.ts" +export async function GET( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) { + const query = req.scope.resolve("query") + + const { data } = await query.graph({ + entity: "wishlist", + fields: ["*", "items.*", "items.product_variant.*"], + filters: { + customer_id: req.auth_context.actor_id, + }, + }) + + if (!data.length) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + "No wishlist found for customer" + ) + } + + return res.json({ + wishlist: data[0], + }) +} +``` + +In this route handler function, you use [Query](!docs!/learn/fundamentals/module-links/query) to retrieve the wishlist of the authenticated customer. For each wishlist, you retrieve its items, and the product variants of those items. + +If the wishlist doesn't exist, you throw an error. Otherwise, you return the wishlist in the response. + +### Test Retrieve Wishlist API Route + +To test the API route, start the Medusa application. + + + +Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry. + + + +Then, send a `GET` request to the `/store/customers/me/wishlists` API route: + +```bash +curl 'localhost:9000/store/customers/me/wishlists' \ +--header 'x-publishable-api-key: {api_key}' \ +--header 'Authorization: Bearer {token}' +``` + +Make sure to replace: + +- `{api_key}` with the publishable API key you copied from the settings, as explained in the [previous step](#retrieve-publishable-api-key). +- `{token}` with the authenticated customer token you received from the [previous step](#retrieve-authenticated-customer-token). + +You'll receive in the response the wishlist of the authenticated customer. + +--- + +## Step 9: Add Item to Wishlist API Route + +Next, you'll add the functionality to add an item to a wishlist. You'll first define a workflow that implements this functionality, then create an API route that executes the workflow. + +### Add Item to Wishlist Workflow + +The workflow to add an item to a wishlist has the following steps: + + + +The `useQueryGraphStep` is from Medusa's workflows package. So, you'll only implement the other steps. + +#### validateWishlistSalesChannelStep + +The second step in the workflow validates that the wishlist belongs to the sales channel specified in the input. + +To create the step, create the file `src/workflows/steps/validate-wishlist-sales-channel.ts` with the following content: + +![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737470093/Medusa%20Resources/wishlist-13_nn924e.jpg) + +```ts title="src/workflows/steps/validate-wishlist-sales-channel.ts" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { InferTypeOf } from "@medusajs/framework/types" +import { Wishlist } from "../../modules/wishlist/models/wishlist" + +type ValidateWishlistSalesChannelStepInput = { + wishlist: InferTypeOf + sales_channel_id: string +} + +export const validateWishlistSalesChannelStep = createStep( + "validate-wishlist-sales-channel", + async (input: ValidateWishlistSalesChannelStepInput, { container }) => { + const { wishlist, sales_channel_id } = input + + if (wishlist.sales_channel_id !== sales_channel_id) { + throw new Error("Wishlist does not belong to the current sales channel") + } + } +) +``` + +This step receives the wishlist object and the sales channel ID as input. In the step function, if the wishlist's sales channel ID doesn't match the sales channel ID in the input, you throw an error. + + + +To represent a data model in a type, use the [InferTypeOf](!docs!/learn/fundamentals/data-models/infer-type) utility. + + + +#### validateVariantWishlistStep + +The next step in the workflow validates that the specified variant is not already in the wishlist. + +Create the file `src/workflows/steps/validate-variant-wishlist.ts` with the following content: + +![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737470156/Medusa%20Resources/wishlist-14_ckoesz.jpg) + +export const validateVariantWishlistHighlights = [ + ["20", "isInWishlist", "Check whether the variant is already in the wishlist."], + ["33", "graph", "Retrieve the variant's details along with the sales channels that its product is available in."], + ["45", "variantInSalesChannel", "Throw an error if the variant isn't in the wishlist's sales channel."] +] + +```ts title="src/workflows/steps/validate-variant-wishlist.ts" highlights={validateVariantWishlistHighlights} +import { InferTypeOf } from "@medusajs/framework/types" +import { Wishlist } from "../../modules/wishlist/models/wishlist" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { MedusaError } from "@medusajs/framework/utils" + +type ValidateVariantWishlistStepInput = { + variant_id: string + sales_channel_id: string + wishlist: InferTypeOf +} + +export const validateVariantWishlistStep = createStep( + "validate-variant-in-wishlist", + async ({ + variant_id, + sales_channel_id, + wishlist, + }: ValidateVariantWishlistStepInput, { container }) => { + // validate whether variant is in wishlist + const isInWishlist = wishlist.items?.some( + (item) => item.product_variant_id === variant_id + ) + + if (isInWishlist) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Variant is already in wishlist" + ) + } + + // validate that the variant is available in the specified sales channel + const query = container.resolve("query") + const { data } = await query.graph({ + entity: "variant", + fields: ["product.sales_channels.*"], + filters: { + id: variant_id, + }, + }) + + const variantInSalesChannel = data[0].product.sales_channels.some( + (sc) => sc.id === sales_channel_id + ) + + if (!variantInSalesChannel) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Variant is not available in the specified sales channel" + ) + } + } +) +``` + +This step receives the variant ID, sales channel ID, and wishlist object as input. In the step function, you throw an error if: + +- The variant is already in the wishlist. +- The variant is not available in the specified sales channel. You use Query to retrieve the sales channels that the variant's product is available in. + +#### createWishlistItemStep + +The fourth step in the workflow creates a wishlist item for the specified variant in the wishlist. + +Create the file `src/workflows/steps/create-wishlist-item.ts` with the following content: + +![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737470302/Medusa%20Resources/wishlist-15_oc696x.jpg) + +export const createWishlistItemStepHighlights = [ + ["16", "createWishlistItems", "Create the wishlist item."], + ["24", "deleteWishlistItems", "Delete the wishlist item if an error occurs in the workflow."] +] + +```ts title="src/workflows/steps/create-wishlist-item.ts" highlights={createWishlistItemStepHighlights} +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import WishlistModuleService from "../../modules/wishlist/service" +import { WISHLIST_MODULE } from "../../modules/wishlist" + +type CreateWishlistItemStepInput = { + wishlist_id: string + product_variant_id: string +} + +export const createWishlistItemStep = createStep( + "create-wishlist-item", + async (input: CreateWishlistItemStepInput, { container }) => { + const wishlistModuleService: WishlistModuleService = + container.resolve(WISHLIST_MODULE) + + const item = await wishlistModuleService.createWishlistItems(input) + + return new StepResponse(item, item.id) + }, + async (id, { container }) => { + const wishlistModuleService: WishlistModuleService = + container.resolve(WISHLIST_MODULE) + + await wishlistModuleService.deleteWishlistItems(id) + } +) +``` + +This step receives the wishlist ID and the variant ID as input. In the step function, you resolve the Wishlist Module's service from the container and use its generated `createWishlistItems` method to create the wishlist item, passing it the input as a parameter. + +You return the created wishlist item and pass the item's ID to the compensation function. In the compensation function, you resolve the Wishlist Module's service from the container and use its generated `deleteWishlistItems` method to delete the wishlist item if an error occurs in the workflow. + +#### Add Item to Wishlist Workflow + +You can now add the `createWishlistItemWorkflow` to the plugin. Create the file `src/workflows/create-wishlist-item.ts` with the following content: + +![Directory structure after adding the workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737470660/Medusa%20Resources/wishlist-16_ovujwp.jpg) + +export const createWishlistItemWorkflowHighlights = [ + ["16", "useQueryGraphStep", "Retrieve the wishlist of the specified customer."], + ["27", "validateWishlistSalesChannelStep", "Validate that the wishlist belongs to the specified sales channel."], + ["33", "validateVariantWishlistStep", "Validate that the specified variant is not already in the wishlist."], + ["39", "createWishlistItemStep", "Create the wishlist item."], + ["45", "useQueryGraphStep", "Retrieve the wishlist again with the new item added."], + ["54", "wishlist", "Return the wishlist with the new item."] +] + +```ts title="src/workflows/create-wishlist-item.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" highlights={createWishlistItemWorkflowHighlights} +import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { validateWishlistSalesChannelStep } from "./steps/validate-wishlist-sales-channel" +import { createWishlistItemStep } from "./steps/create-wishlist-item" +import { validateVariantWishlistStep } from "./steps/validate-variant-wishlist" + +type CreateWishlistItemWorkflowInput = { + variant_id: string + customer_id: string + sales_channel_id: string +} + +export const createWishlistItemWorkflow = createWorkflow( + "create-wishlist-item", + (input: CreateWishlistItemWorkflowInput) => { + const { data: wishlists } = useQueryGraphStep({ + entity: "wishlist", + fields: ["*", "items.*"], + filters: { + customer_id: input.customer_id, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + validateWishlistSalesChannelStep({ + wishlist: wishlists[0], + sales_channel_id: input.sales_channel_id, + }) + + + validateVariantWishlistStep({ + variant_id: input.variant_id, + sales_channel_id: input.sales_channel_id, + wishlist: wishlists[0], + }) + + createWishlistItemStep({ + product_variant_id: input.variant_id, + wishlist_id: wishlists[0].id, + }) + + // refetch wishlist + const { data: updatedWishlists } = useQueryGraphStep({ + entity: "wishlist", + fields: ["*", "items.*", "items.product_variant.*"], + filters: { + id: wishlists[0].id, + }, + }).config({ name: "refetch-wishlist" }) + + return new WorkflowResponse({ + wishlist: updatedWishlists[0], + }) + } +) +``` + +You create a `createWishlistItemWorkflow`. In the workflow, you: + +- Use the [useQueryGraphStep](/references/helper-steps/useQueryGraphStep) to retrieve the wishlist of a customer. Notice that you pass the link definition between a wishlist and a customer as an entry point to Query. This allows you to filter the wishlist by the customer ID. +- Use the `validateWishlistSalesChannelStep` step to validate that the wishlist belongs to the sales channel specified in the input. +- Use the `validateVariantWishlistStep` step to validate that the variant specified in the input is not already in the wishlist. +- Use the `createWishlistItemStep` step to create the wishlist item. +- Use the `useQueryGraphStep` again to retrieve the wishlist with the new item added. + +You return the wishlist with its items. + +### Add Item to Wishlist API Route + +You'll now create an API route that executes the `createWishlistItemWorkflow` to add an item to a wishlist. + +Create the file `src/api/store/customers/me/wishlists/items/route.ts` with the following content: + +![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737470985/Medusa%20Resources/wishlist-17_zmqk6c.jpg) + +```ts title="src/api/store/customers/me/wishlists/items/route.ts" +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework" +import { createWishlistItemWorkflow } from "../../../../../../workflows/create-wishlist-item" +import { MedusaError } from "@medusajs/framework/utils" + +type PostStoreCreateWishlistItemType = { + variant_id: string +} + +export async function POST( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) { + if (!req.publishable_key_context?.sales_channel_ids.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "At least one sales channel ID is required to be associated with the publishable API key in the request header." + ) + } + const { result } = await createWishlistItemWorkflow(req.scope) + .run({ + input: { + variant_id: req.validatedBody.variant_id, + customer_id: req.auth_context.actor_id, + sales_channel_id: req.publishable_key_context?.sales_channel_ids[0], + }, + }) + + res.json({ + wishlist: result.wishlist, + }) +} +``` + +This route exposes a `POST` endpoint at `/store/customers/me/wishlists/items`. Notice that the `AuthenticatedMedusaRequest` accepts a type parameter indicating the type of the accepted request body. In this case, the request body must have a `variant_id` property, indicating the ID of the variant to add to the wishlist. + +In the route handler function, you execute the `createWishlistItemWorkflow` workflow, passing the authenticated customer ID, the variant ID, and the sales channel ID as input. You return in the response the updated wishlist. + +### Add Validation Schema + +To ensure that a variant ID is passed in the body of requests sent to this API route, you'll define a validation schema for the request body. + +In Medusa, you create validation schemas using [Zod](https://zod.dev/) in a TypeScript file under the `src/api` directory. So, create the file `src/api/store/customers/me/wishlists/items/validators.ts` with the following content: + +![Directory structure after adding the validation schema file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737471383/Medusa%20Resources/wishlist-18_hj9iom.jpg) + +```ts title="src/api/store/customers/me/wishlists/items/validators.ts" +import { z } from "zod" + +export const PostStoreCreateWishlistItem = z.object({ + variant_id: z.string(), +}) +``` + +You create an object schema with a `variant_id` property of type `string`. + + + +Learn more about creating schemas in [Zod's documentation](https://zod.dev/). + + + +You can now replace the `PostStoreCreateWishlistItemType` type in `src/api/store/customers/me/wishlists/items/route.ts` with the following: + +```ts title="src/api/store/customers/me/wishlists/items/route.ts" +// ... +import { z } from "zod" +import { PostStoreCreateWishlistItem } from "./validators" + +type PostStoreCreateWishlistItemType = z.infer< + typeof PostStoreCreateWishlistItem +> +``` + +Finally, to use the schema for validation, you need to apply the `validateAndTransformBody` middleware on the `/store/customers/me/wishlists/items` route. A middleware is a function executed before the API route when a request is sent to it. + +The `validateAndTransformBody` middleware is available out-of-the-box in Medusa, allowing you to validate and transform the request body using a Zod schema. + + + +Learn more about middlewares in [this documentation](!docs!/learn/fundamentals/api-routes/middlewares). + + + +To apply the middleware, create the file `src/api/middlewares.ts` with the following content: + +![Directory structure after adding the middleware file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737471615/Medusa%20Resources/wishlist-19_ryyzdk.jpg) + +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformBody, +} from "@medusajs/framework/http" +import { + PostStoreCreateWishlistItem, +} from "./store/customers/me/wishlists/items/validators" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/store/customers/me/wishlists/items", + method: "POST", + middlewares: [ + validateAndTransformBody(PostStoreCreateWishlistItem), + ], + }, + ], +}) +``` + +In this file, you export the middlewares definition using `defineMiddlewares` from the Medusa Framework. This function accepts an object having a `routes` property, which is an array of middleware configurations to apply on routes. + +You pass in the `routes` array an object having the following properties: + +- `matcher`: The route to apply the middleware on. +- `method`: The HTTP method to apply the middleware on for the specified API route. +- `middlewares`: An array of the middlewares to apply. You apply the following middleware: + - `validateAndTransformBody`: A middleware to ensure the received request body is valid against the Zod schema you defined earlier. + +Any request sent to `/store/customers/me/wishlists/items` will now automatically fail if its body parameters don't match the `PostStoreCreateWishlistItem` validation schema. + +### Test API Route + +Start the Medusa application to test out the API route. + + + +Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry. + + + +#### Retrieve Variant ID + +To retrieve an ID of a variant to add to the wishlist, send a `GET` request to the `/store/products` API route: + +```bash +curl 'localhost:9000/store/products' \ +--header 'x-publishable-api-key: {api_key}' +``` + +Make sure to replace `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key). + +The response will contain a list of products. You can use the `id` of a product's variant to add to the wishlist. + +#### Add Variant to Wishlist + +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' \ +--header 'Content-Type: application/json' \ +--header 'x-publishable-api-key: {api_key}' \ +--header 'Authorization: Bearer {token}' \ +--data-raw '{ + "variant_id": "{variant_id}" +}' +``` + +Make sure to replace: + +- `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key). +- `{token}` with the authenticated customer token, as explained in [a previous section](#retrieve-authenticated-customer-token). +- `{variant_id}` with the ID of the variant you retrieved from the `/store/products` API route. + +You'll receive in the response the updated wishlist with the added item. + +--- + +## Step 10: Remove Item from Wishlist API Route + +In this step, you'll add the functionality to remove an item from a wishlist. You'll first define a workflow that implements this functionality, then create an API route that executes the workflow. + +### Remove Item from Wishlist Workflow + +The workflow to remove an item from a wishlist has the following steps: + + + +The `useQueryGraphStep` is from Medusa's workflows package. So, you'll only implement the other steps. + +#### validateItemInWishlistStep + +The second step of the workflow validates that the item to remove is in the authenticated customer's wishlist. + +To create the step, create the file `src/workflows/steps/validate-item-in-wishlist.ts` with the following content: + +![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737474621/Medusa%20Resources/wishlist-20_jcwrtf.jpg) + +```ts title="src/workflows/steps/validate-item-in-wishlist.ts" +import { InferTypeOf } from "@medusajs/framework/types" +import { Wishlist } from "../../modules/wishlist/models/wishlist" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { MedusaError } from "@medusajs/framework/utils" + +type ValidateItemInWishlistStepInput = { + wishlist: InferTypeOf + wishlist_item_id: string +} + +export const validateItemInWishlistStep = createStep( + "validate-item-in-wishlist", + async ({ + wishlist, + wishlist_item_id, + }: ValidateItemInWishlistStepInput, { container }) => { + const item = wishlist.items.find((item) => item.id === wishlist_item_id) + + if (!item) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Item does not exist in customer's wishlist" + ) + } + } +) +``` + +This step receives the wishlist object and the wishlist item ID as input. In the step function, you find the item in the wishlist by its ID. If the item doesn't exist, you throw an error. + +#### deleteWishlistItemStep + +The third step of the workflow deletes the item from the wishlist. + +Create the file `src/workflows/steps/delete-wishlist-item.ts` with the following content: + +![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737474703/Medusa%20Resources/wishlist-21_e50lrg.jpg) + +```ts title="src/workflows/steps/delete-wishlist-item.ts" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import WishlistModuleService from "../../modules/wishlist/service" +import { WISHLIST_MODULE } from "../../modules/wishlist" + +type DeleteWishlistItemStepInput = { + wishlist_item_id: string +} + +export const deleteWishlistItemStep = createStep( + "delete-wishlist-item", + async ({ wishlist_item_id }: DeleteWishlistItemStepInput, { container }) => { + const wishlistModuleService: WishlistModuleService = + container.resolve(WISHLIST_MODULE) + + await wishlistModuleService.softDeleteWishlistItems(wishlist_item_id) + + return new StepResponse(void 0, wishlist_item_id) + }, + async (wishlistItemId, { container }) => { + const wishlistModuleService: WishlistModuleService = + container.resolve(WISHLIST_MODULE) + + await wishlistModuleService.restoreWishlistItems([wishlistItemId]) + } +) +``` + +This step receives the wishlist item ID as input. In the step function, you resolve the Wishlist Module's service from the container and use its generated `softDeleteWishlistItems` method to delete the wishlist item. + +You pass the deleted wishlist item ID to the compensation function. In the compensation function, you resolve the Wishlist Module's service from the container and use its generated `restoreWishlistItems` method to restore the wishlist item if an error occurs in the workflow. + + + +Learn more about the [softDelete](../../../service-factory-reference/methods/soft-delete/page.mdx) and [restore](../../../service-factory-reference/methods/restore/page.mdx) generated methods. + + + +#### Remove Item from Wishlist Workflow + +You can now add the `deleteWishlistItemWorkflow` to the plugin. Create the file `src/workflows/delete-wishlist-item.ts` with the following content: + +![Directory structure after adding the workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737474872/Medusa%20Resources/wishlist-22_wt1g36.jpg) + +export const deleteWishlistItemWorkflowHighlights = [ + ["14", "useQueryGraphStep", "Retrieve the wishlist of a customer."], + ["25", "validateItemInWishlistStep", "Validate that the item is in the customer's wishlist."], + ["30", "deleteWishlistItemStep", "Delete the wishlist item."], + ["33", "useQueryGraphStep", "Retrieve the wishlist again with the item removed."], + ["42", "wishlist", "Return the wishlist without the removed item."] +] + +```ts title="src/workflows/delete-wishlist-item.ts" highlights={deleteWishlistItemWorkflowHighlights} +import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { deleteWishlistItemStep } from "./steps/delete-wishlist-item" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { validateItemInWishlistStep } from "./steps/validate-item-in-wishlist" + +type DeleteWishlistItemWorkflowInput = { + wishlist_item_id: string + customer_id: string +} + +export const deleteWishlistItemWorkflow = createWorkflow( + "delete-wishlist-item", + (input: DeleteWishlistItemWorkflowInput) => { + const { data: wishlists } = useQueryGraphStep({ + entity: "wishlist", + fields: ["*", "items.*"], + filters: { + customer_id: input.customer_id, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + validateItemInWishlistStep({ + wishlist: wishlists[0], + wishlist_item_id: input.wishlist_item_id, + }) + + deleteWishlistItemStep(input) + + // refetch wishlist + const { data: updatedWishlists } = useQueryGraphStep({ + entity: "wishlist", + fields: ["*", "items.*", "items.product_variant.*"], + filters: { + id: wishlists[0].wishlist.id, + }, + }).config({ name: "refetch-wishlist" }) + + return new WorkflowResponse({ + wishlist: updatedWishlists[0], + }) + } +) +``` + +You create a `deleteWishlistItemWorkflow`. In the workflow, you: + +- Use the [useQueryGraphStep](/references/helper-steps/useQueryGraphStep) to retrieve the wishlist of a customer. Notice that you pass the link definition between a wishlist and a customer as an entry point to Query. This allows you to filter the wishlist by the customer ID. +- Use the `validateItemInWishlistStep` step to validate that the item to remove is in the customer's wishlist. +- Use the `deleteWishlistItemStep` step to delete the item from the wishlist. +- Use the `useQueryGraphStep` again to retrieve the wishlist with the item removed. + +You return the wishlist without the removed item. + +### Remove Item from Wishlist API Route + +You'll now create an API route that executes the `deleteWishlistItemWorkflow` to remove an item from a wishlist. + +Create the file `src/api/store/customers/me/wishlists/items/[id]/route.ts` with the following content: + +![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737475074/Medusa%20Resources/wishlist-23_qatcia.jpg) + +```ts title="src/api/store/customers/me/wishlists/items/[id]/route.ts" +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework" +import { deleteWishlistItemWorkflow } from "../../../../../../../workflows/delete-wishlist-item" + +export async function DELETE( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) { + const { result } = await deleteWishlistItemWorkflow(req.scope) + .run({ + input: { + wishlist_item_id: req.params.id, + customer_id: req.auth_context.actor_id, + }, + }) + + res.json({ + wishlist: result.wishlist, + }) +} +``` + +This route exposes a `DELETE` endpoint at `/store/customers/me/wishlists/items/:id`. The `:id` parameter in the route path represents the ID of the wishlist item to remove. + +In the route handler function, you execute the `deleteWishlistItemWorkflow` workflow, passing the authenticated customer ID and the wishlist item ID as input. You return in the response the updated wishlist. + +### Test API Route + +Start the Medusa application to test out the API route. + + + +Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry. + + + +#### Retrieve Wishlist Item ID + +To retrieve an ID of a wishlist item to remove, send a `GET` request to the `/store/customers/me/wishlists` API route: + +```bash +curl 'localhost:9000/store/customers/me/wishlists' \ +--header 'x-publishable-api-key: {api_key}' \ +--header 'Authorization: Bearer {token}' +``` + +Make sure to replace: + +- `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key). +- `{token}` with the authenticated customer token, as explained in [a previous section](#retrieve-authenticated-customer-token). + +The response will contain the wishlist of the authenticated customer. You can use the `id` of an item in the wishlist to remove. + +#### Remove Item from Wishlist + +Then, send a `DELETE` request to the `/store/customers/me/wishlists/items/:id` API route to remove the item from the wishlist: + +```bash +curl -X DELETE 'localhost:9000/store/customers/me/wishlists/items/{item_id}' \ +--header 'x-publishable-api-key: {api_key}' \ +--header 'Authorization: Bearer {token}' +``` + +Make sure to replace: + +- `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key). +- `{token}` with the authenticated customer token, as explained in [a previous section](#retrieve-authenticated-customer-token). +- `{item_id}` with the ID of the item you retrieved from the `/store/customers/me/wishlists` API route. + +You'll receive in the response the updated wishlist without the removed item. + +--- + +## Step 11: Share Wishlist API Route + +In this step, you'll add the functionality to allow customers to share their wishlist with others. The route will return a token that can be passed to another API route that you'll create in the next step to retrieve the shared wishlist. + +To create the token and decode it later, you'll use the [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken) package. So, run the following command in the plugin project to install the package: + +```bash npm2yarn title="Plugin project" +npm install jsonwebtoken +``` + +Then, to create the API route, create the file `src/api/store/customers/me/wishlists/share/route.ts` with the following content: + +![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737475331/Medusa%20Resources/wishlist-24_tiwjpr.jpg) + +```ts title="src/api/store/customers/me/wishlists/share/route.ts" +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework" +import { MedusaError } from "@medusajs/framework/utils" +import jwt from "jsonwebtoken" + +export async function POST( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) { + if (!req.publishable_key_context?.sales_channel_ids.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "At least one sales channel ID is required to be associated with the publishable API key in the request header." + ) + } + + const query = req.scope.resolve("query") + + const { data } = await query.graph({ + entity: "wishlist", + fields: ["*"], + filters: { + customer_id: req.auth_context.actor_id, + }, + }) + + if (!data.length) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + "No wishlist found for customer" + ) + } + + if (data[0].sales_channel_id !== req.publishable_key_context.sales_channel_ids[0]) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Wishlist does not belong to the specified sales channel" + ) + } + + // TODO generate the token +} +``` + +This route exposes a `POST` endpoint at `/store/customers/me/wishlists/share`. In the route handler function, you use Query to retrieve the wishlist of the authenticated customer. If the customer doesn't have a wishlist, or the wishlist doesn't belong to the sales channel specified in the request's publishable API key, you throw an error. + +You'll now generate a token that contains the wishlist ID. To do this, replace the `TODO` in the route handler function with the following: + +```ts title="src/api/store/customers/me/wishlists/share/route.ts" +const { http } = req.scope.resolve("configModule").projectConfig + +const wishlistToken = jwt.sign({ + wishlist_id: data[0].id, +}, http.jwtSecret!, { + expiresIn: http.jwtExpiresIn, +}) + +return res.json({ + token: wishlistToken, +}) +``` + +You first retrieve the [http Medusa configuration](/references/medusa-config#http) which holds configurations related to JWT secrets and expiration times. You then use the `jsonwebtoken` package to sign a token containing the wishlist ID. You return the token in the response. + +### Test API Route + +Start the Medusa application to test out the API route. + + + +Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry. + + + +Then, send a `POST` request to the `/store/customers/me/wishlists/share` API route to generate a share token for the authenticated customer's wishlist: + +```bash +curl -X POST 'localhost:9000/store/customers/me/wishlists/share' \ +--header 'x-publishable-api-key: {api_key}' \ +--header 'Authorization: Bearer {token}' +``` + +Make sure to replace: + +- `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key). +- `{token}` with the authenticated customer token, as explained in [a previous section](#retrieve-authenticated-customer-token). + +You'll receive in the response a token that you can pass to the next API route to retrieve the shared wishlist. + +--- + +## Step 12: Retrieve Shared Wishlist API Route + +In this step, you'll add an API route that retrieves a wishlist shared using a token returned by the `/store/customers/me/wishlists/share` API route. + +Create the file `src/api/store/wishlists/[token]/route.ts` with the following content: + +![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737475795/Medusa%20Resources/wishlist-25_sodzsr.jpg) + +```ts title="src/api/store/wishlists/[token]/route.ts" +import { MedusaResponse, MedusaStoreRequest } from "@medusajs/framework" +import { MedusaError } from "@medusajs/framework/utils" +import { decode, JwtPayload } from "jsonwebtoken" + +export async function GET( + req: MedusaStoreRequest, + res: MedusaResponse +) { + if (!req.publishable_key_context?.sales_channel_ids.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "At least one sales channel ID is required to be associated with the publishable API key in the request header." + ) + } + + const decodedToken = decode(req.params.token) as JwtPayload + + if (!decodedToken.wishlist_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Invalid token" + ) + } + + const query = req.scope.resolve("query") + + const { data } = await query.graph({ + entity: "wishlist", + fields: ["*", "items.*", "items.product_variant.*"], + filters: { + id: decodedToken.wishlist_id, + }, + }) + + if (!data.length) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + "No wishlist found" + ) + } + + if (data[0].sales_channel_id !== req.publishable_key_context.sales_channel_ids[0]) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Wishlist does not belong to the request's sales channel" + ) + } + + res.json({ + wishlist: data[0], + }) +} +``` + +This route exposes a `GET` endpoint at `/store/wishlists/:token`. The `:token` parameter in the route path represents the token generated by the `/store/customers/me/wishlists/share` API route. + +In the route handler function, you decode the token to retrieve the wishlist ID. If the token is invalid, you throw an error. + +Then, you use Query to retrieve the wishlist with the ID from the decoded token. If no wishlist is found or the wishlist doesn't belong to the sales channel ID of the current request, you throw an error. + +You return in the response the shared wishlist. + +### Test API Route + +Start the Medusa application to test out the API route. + + + +Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry. + + + +Then, send a `GET` request to the `/store/wishlists/:token` API route to retrieve the shared wishlist: + +```bash +curl 'localhost:9000/store/wishlists/{wishlist_token}' \ +--header 'x-publishable-api-key: {api_key}' +``` + +Make sure to replace: + +- `{wishlist_token}` with the token you received from the `/store/customers/me/wishlists/share` API route. +- `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key). + +You'll receive in the response the shared wishlist. + +--- + +## Step 13: Show Wishlist Count in Medusa Admin + +In this step, you'll customize the Medusa Admin dashboard to show for each product the number of wishlists it's in. + +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. + + + +Learn more about widgets in [this documentation](!docs!/learn/fundamentals/admin/widgets). + + + +### Add Method to Retrieve Wishlist Count + +To retrieve the number of wishlists a product is in, you'll add a method to the `WishlistModuleService` that runs a query to retrieve distinct wishlist IDs containing a product variant. + +In `src/modules/wishlist/service.ts`, add the following imports and method: + +```ts title="src/modules/wishlist/service.ts" +// other imports... +import { InjectManager } from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" + +export default class WishlistModuleService extends MedusaService({ + Wishlist, + WishlistItem, +}) { + @InjectManager() + async getWishlistsOfVariants( + variantIds: string[], + @MedusaContext() context: Context = {} + ): Promise { + return (await context.manager?.createQueryBuilder("wishlist_item", "wi") + .select(["wi.wishlist_id"], true) + .where("wi.product_variant_id IN (?)", [variantIds]) + .execute())?.length || 0 + } +} +``` + +To perform queries on the database in a method, add the `@InjectManager` decorator to the method. This will inject a [forked MikroORM entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager) that you can use in your method. + +Methods with the `@InjectManager` decorator accept as a last parameter a context object that has the `@MedusaContext` decorator. The entity manager is injected into the `manager` property of this paramter. + +The method accepts an array of variant IDs as a parameter. In the method, you use the `createQueryBuilder` to construct a query, passing it the name of the `WishlistItem`'s table. You then select distinct `wishlist_id`s where the `product_variant_id` of the wishlist item is in the array of variant IDs. + +You execute the query and return the number of distinct wishlist IDs containing the product variants. You'll use this method next. + +### Create Wishlist Count API Route + +Before creating the widget, you'll create the API route that retrieves the number of wishlists a product is in. + +Create the file `src/api/store/products/[id]/wishlist/route.ts` with the following content: + +![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737630927/Medusa%20Resources/wishlist-26_ervnfg.jpg) + +export const wishlistCountRouteHighlights = [ + ["17", "graph", "Retrieve the product and its variants."], + ["25", "", "If product doesn't exist, throw an error."], + ["32", "count", "Retrieve the number of wishlists that the product is in."] +] + +```ts title="src/api/store/products/[id]/wishlist/route.ts" highlights={wishlistCountRouteHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework" +import WishlistModuleService from "../../../../../modules/wishlist/service" +import { WISHLIST_MODULE } from "../../../../../modules/wishlist" +import { MedusaError } from "@medusajs/framework/utils" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { id } = req.params + + const query = req.scope.resolve("query") + const wishlistModuleService: WishlistModuleService = req.scope.resolve( + WISHLIST_MODULE + ) + + const { data: [product] } = await query.graph({ + entity: "product", + fields: ["variants.*"], + filters: { + id, + }, + }) + + if (!product) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Product with id: ${id} was not found` + ) + } + + const count = await wishlistModuleService.getWishlistsOfVariants( + product.variants.map((v) => v.id) + ) + + res.json({ + count, + }) +} +``` + +This route exposes a `GET` endpoint at `/store/products/:id/wishlist`. The `:id` parameter in the route path represents the ID of the product to retrieve the wishlist count for. + +In the route handler function, you use Query to retrieve the product and its variants, and throw an error if the product doesn't exist. + +Then, you resolve the `WishlistModuleService` from the Medusa Container and use its `getWishlistsOfVariants` method to retrieve the number of wishlists the product's variants are in. You return the count in the response. + +You'll use this API route in the widget next. + +### Create Wishlist Count Widget + +You'll now create the widget that will be shown on a product's page in the Medusa Admin. + +In the widget, you'll send a request to the API route you created to retrieve the wishlist count for the product. To send the request, you'll use the [JS SDK](../../../js-sdk/page.mdx), which is a JavaScript library that simplifies sending requests to Medusa's API routes. + +To initialize the JS SDK, create the file `src/admin/lib/sdk.ts` with the following content: + +![Directory structure after adding the SDK file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737631853/Medusa%20Resources/wishlist-27_pkzeaj.jpg) + +```ts title="src/admin/lib/sdk.ts" +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + baseUrl: "http://localhost:9000", + debug: process.env.NODE_ENV === "development", + auth: { + type: "session", + }, +}) +``` + +You initialize an instance of the JS SDK, which you'll use in the widget to send requests. + + + +Learn more about the JS SDK and configuring it in [this documentation](../../../js-sdk/page.mdx). + + + +Then, to create the widget, create the file `src/admin/widgets/product-widget.tsx` with the following content: + +![Directory structure after adding the widget file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737631988/Medusa%20Resources/wishlist-28_fx8rw7.jpg) + +export const widgetHighlights = [ + ["10", "WishlistResponse", "Define the type of expected response."], + ["14", "ProductWidget", "Define and export the widget's React component."], + ["15", "data", "Receive the product's details as a prop."], + ["17", "useQuery", "Retrieve the wishlist count from the API route."], + ["29", "", "Display the count."], + ["36", "config", "Export the widget's content."], + ["37", "zone", "Inject the widget at the top of a product's details page."] +] + +```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading, Text } from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { sdk } from "../lib/sdk" +import { + DetailWidgetProps, + AdminProduct, +} from "@medusajs/framework/types" + +type WishlistResponse = { + count: number +} + +const ProductWidget = ({ + data: product, +}: DetailWidgetProps) => { + const { data, isLoading } = useQuery({ + queryFn: () => sdk.client.fetch(`/admin/products/${product.id}/wishlist`), + queryKey: [["products", product.id, "wishlist"]], + }) + + return ( + +
+ Wishlist +
+ + {isLoading ? + "Loading..." : `This product is in ${data?.count} wishlist(s).` + } + +
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +A widget file must export a React component and a `config` object created with `defineWidgetConfig` from the Admin Extension SDK. In the `config` object, you specify the zone to inject the widget into in the `zone` property. This widget is injected into a product's page before any other sections. + + + +Find all widget injection zones in [this reference](../../../admin-widget-injection-zones/page.mdx). + + + +Since the widget is injected into a product's details page, it receives the product's details as a `data` prop. In the widget, you use [Tanstack Query](https://tanstack.com/query/latest) to benefit from features like data caching and invalidation. You use the `useQuery` hook to send a request to the API route you created to retrieve the wishlist count for the product. + +Finally, you display the widget's content using components from [Medusa UI](!ui!), allowing you to align the design of your widget with the Medusa Admin's design system. + +### Test it Out + +To test it out, start the Medusa application. + + + +Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry. + + + +Then: + +1. open the Medusa Admin at `localhost:9000/app` and log in. + +2. Click on Products in the sidebar, then choose a product from the table. + +![Click on the "Products" in the sidebar on the right, then choose a product from the table shown in the middle](https://res.cloudinary.com/dza7lstvk/image/upload/v1737632826/Medusa%20Resources/Screenshot_2025-01-23_at_1.46.29_PM_xjsn8s.png) + +3. You should see the widget you created showing the number of wishlists the product is in at the top of the page. + +![The widget is shown at the top of the product page before other sections](https://res.cloudinary.com/dza7lstvk/image/upload/v1737632826/Medusa%20Resources/Screenshot_2025-01-23_at_1.46.05_PM_hfyz7u.png) + +--- + +## Next Steps + +You've now implemented the wishlist functionality in a Medusa plugin. You can publish that plugin as explained in [this documentation](!docs!/learn/fundamentals/plugins/create#5-publish-plugin-to-npm) to NPM and install it in any Medusa application. This will allow you to re-use your plugin or share it with the community. + +If you're new to Medusa, check out the [main documentation](!docs!/learn), where you'll get a more in-depth learning of all the concepts you've used in this guide and more. + +To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](../../../commerce-modules/page.mdx). + +For other general guides related to [deployment](../../../deployment/page.mdx), [storefront development](../../../storefront-development/page.mdx), [integrations](../../../integrations/page.mdx), and more, check out the [Development Resources](../../../page.mdx). diff --git a/www/apps/resources/app/plugins/page.mdx b/www/apps/resources/app/plugins/page.mdx new file mode 100644 index 0000000000000..45ee1dc7e358e --- /dev/null +++ b/www/apps/resources/app/plugins/page.mdx @@ -0,0 +1,21 @@ +import { ChildDocs } from "docs-ui" + +export const metadata = { + title: `Plugins`, +} + +# {metadata.title} + +A plugin is a package of re-usable Medusa customizations, including [modules](!docs!/learn/fundamentals/modules), [workflows](!docs!/learn/fundamentals/workflows), [API routes](!docs!/learn/fundamentals/api-routes), and more. + +Plugins are useful if you want to re-use your customizations across Medusa applications, or you want to share those customizations with the community. You publish your plugin to NPM, then install it in a Medusa application to use its features. + + + +Learn more about plugins and their difference from modules in [this documentation](!docs!/learn/fundamentals/plugins). + + + +## Guides + + diff --git a/www/apps/resources/app/recipes/commerce-automation/restock-notification/page.mdx b/www/apps/resources/app/recipes/commerce-automation/restock-notification/page.mdx index 825ef3b9622d3..38a1e6209b850 100644 --- a/www/apps/resources/app/recipes/commerce-automation/restock-notification/page.mdx +++ b/www/apps/resources/app/recipes/commerce-automation/restock-notification/page.mdx @@ -212,7 +212,7 @@ So, the `RestockModuleService` class now has methods like `createRestockSubscrip -Find all methods generated by the `MedusaService` in [this reference](../../..//service-factory-reference/page.mdx). +Find all methods generated by the `MedusaService` in [this reference](../../../service-factory-reference/page.mdx). @@ -330,7 +330,7 @@ You define a link using `defineLink` from the Modules SDK. It accepts three para 1. The first data model part of the link, which is the Restock Module's `restockSubscription` data model. A module has a special `linkable` property that contain link configurations for its data models. You also specify the field that points to the product variant. 1. The second data model part of the link, which is the Product Module's `productVariant` data model. -3. An object of configurations for the module link. By default, Medusa creates a table in the database to represent the link you define. However, in this guide, you only want this link to retrieve the variants associated with a subscription and vice-versa. So, you enable `readOnly` telling Medusa not to create a table for this link. +3. An object of configurations for the module link. By default, Medusa creates a table in the database to represent the link you define. However, in this guide, you only want this link to retrieve the variants associated with a subscription. So, you enable `readOnly` telling Medusa not to create a table for this link. In the next steps, you'll see how this link allows you to retrieve product variants' details when retrieving restock subscriptions. diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index 72afd1d7a4241..b27490824676c 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -100,7 +100,7 @@ export const generatedEditDates = { "app/commerce-modules/user/page.mdx": "2025-01-09T13:41:05.543Z", "app/commerce-modules/page.mdx": "2024-12-23T14:38:21.064Z", "app/contribution-guidelines/docs/page.mdx": "2024-12-12T11:06:12.250Z", - "app/create-medusa-app/page.mdx": "2025-01-06T09:14:55.483Z", + "app/create-medusa-app/page.mdx": "2025-01-16T10:00:25.975Z", "app/deployment/admin/vercel/page.mdx": "2024-10-16T08:10:29.377Z", "app/deployment/medusa-application/railway/page.mdx": "2024-11-11T11:50:10.517Z", "app/deployment/storefront/vercel/page.mdx": "2025-01-06T12:19:31.142Z", @@ -593,11 +593,11 @@ export const generatedEditDates = { "references/types/types.NotificationTypes/page.mdx": "2024-11-25T17:49:28.027Z", "app/medusa-cli/commands/db/page.mdx": "2025-01-16T07:34:08.014Z", "app/medusa-cli/commands/develop/page.mdx": "2024-08-28T10:43:45.452Z", - "app/medusa-cli/commands/exec/page.mdx": "2024-08-28T10:45:31.229Z", + "app/medusa-cli/commands/exec/page.mdx": "2025-01-16T09:51:17.050Z", "app/medusa-cli/commands/new/page.mdx": "2024-08-28T10:43:34.110Z", - "app/medusa-cli/commands/start-cluster/page.mdx": "2024-08-28T11:25:05.257Z", + "app/medusa-cli/commands/start-cluster/page.mdx": "2025-01-16T09:51:19.838Z", "app/medusa-cli/commands/start/page.mdx": "2024-08-28T10:44:19.952Z", - "app/medusa-cli/commands/telemtry/page.mdx": "2024-08-28T11:25:08.553Z", + "app/medusa-cli/commands/telemtry/page.mdx": "2025-01-16T09:51:24.323Z", "app/medusa-cli/commands/user/page.mdx": "2024-08-28T10:44:52.489Z", "app/recipes/marketplace/examples/restaurant-delivery/page.mdx": "2024-12-11T10:05:52.851Z", "references/types/HttpTypes/interfaces/types.HttpTypes.AdminCreateCustomerGroup/page.mdx": "2024-12-09T13:21:33.569Z", @@ -5609,7 +5609,7 @@ export const generatedEditDates = { "references/modules/sales_channel_models/page.mdx": "2024-12-10T14:55:13.205Z", "references/types/DmlTypes/types/types.DmlTypes.KnownDataTypes/page.mdx": "2024-12-17T16:57:19.922Z", "references/types/DmlTypes/types/types.DmlTypes.RelationshipTypes/page.mdx": "2024-12-10T14:54:55.435Z", - "app/recipes/commerce-automation/restock-notification/page.mdx": "2024-12-11T08:47:27.471Z", + "app/recipes/commerce-automation/restock-notification/page.mdx": "2025-01-23T10:18:28.126Z", "app/troubleshooting/workflow-errors/page.mdx": "2024-12-11T08:44:36.598Z", "app/integrations/guides/shipstation/page.mdx": "2025-01-07T14:40:42.561Z", "app/nextjs-starter/guides/customize-stripe/page.mdx": "2024-12-25T14:48:55.877Z", @@ -5866,6 +5866,7 @@ export const generatedEditDates = { "references/types/types/types.CreateProductVariantWorkflowInputDTO/page.mdx": "2025-01-13T17:30:29.301Z", "references/types/types/types.CreateProductWorkflowInputDTO/page.mdx": "2025-01-13T17:30:29.302Z", "references/types/types/types.UpdateProductVariantWorkflowInputDTO/page.mdx": "2025-01-13T17:30:29.302Z", + "app/medusa-cli/commands/plugin/page.mdx": "2025-01-16T09:58:16.179Z", "references/inventory_next/interfaces/inventory_next.UpdateInventoryLevelInput/page.mdx": "2025-01-17T16:43:28.933Z", "references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemLocationLevels/page.mdx": "2025-01-17T16:43:31.720Z", "references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemsLocationLevels/page.mdx": "2025-01-17T16:43:31.721Z", @@ -5887,5 +5888,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-01-17T16:43:26.124Z", + "app/plugins/guides/wishlist/page.mdx": "2025-01-23T11:59:10.008Z", + "app/plugins/page.mdx": "2025-01-22T09:36:37.745Z", "app/admin-components/components/data-table/page.mdx": "2025-01-22T16:01:01.279Z" } \ No newline at end of file diff --git a/www/apps/resources/generated/files-map.mjs b/www/apps/resources/generated/files-map.mjs index 181668542a6aa..0ba01ecf749fd 100644 --- a/www/apps/resources/generated/files-map.mjs +++ b/www/apps/resources/generated/files-map.mjs @@ -823,6 +823,10 @@ export const filesMap = [ "filePath": "/www/apps/resources/app/medusa-cli/commands/new/page.mdx", "pathname": "/medusa-cli/commands/new" }, + { + "filePath": "/www/apps/resources/app/medusa-cli/commands/plugin/page.mdx", + "pathname": "/medusa-cli/commands/plugin" + }, { "filePath": "/www/apps/resources/app/medusa-cli/commands/start/page.mdx", "pathname": "/medusa-cli/commands/start" @@ -863,6 +867,14 @@ export const filesMap = [ "filePath": "/www/apps/resources/app/page.mdx", "pathname": "/" }, + { + "filePath": "/www/apps/resources/app/plugins/guides/wishlist/page.mdx", + "pathname": "/plugins/guides/wishlist" + }, + { + "filePath": "/www/apps/resources/app/plugins/page.mdx", + "pathname": "/plugins" + }, { "filePath": "/www/apps/resources/app/recipes/b2b/page.mdx", "pathname": "/recipes/b2b" diff --git a/www/apps/resources/generated/sidebar.mjs b/www/apps/resources/generated/sidebar.mjs index 53a16279eea59..30563b6f25179 100644 --- a/www/apps/resources/generated/sidebar.mjs +++ b/www/apps/resources/generated/sidebar.mjs @@ -15942,6 +15942,41 @@ export const generatedSidebar = [ } ] }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "path": "/plugins", + "title": "Plugins", + "isChildSidebar": true, + "children": [ + { + "loaded": true, + "isPathHref": true, + "type": "link", + "title": "Overview", + "path": "/plugins", + "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "type": "category", + "title": "Guides", + "children": [ + { + "loaded": true, + "isPathHref": true, + "type": "link", + "title": "Wishlist", + "path": "/plugins/guides/wishlist", + "description": "Learn how to build a wishlist plugin.", + "children": [] + } + ] + } + ] + }, { "loaded": true, "isPathHref": true, @@ -16456,6 +16491,15 @@ export const generatedSidebar = [ "description": "", "children": [] }, + { + "loaded": true, + "isPathHref": true, + "type": "link", + "path": "/medusa-cli/commands/plugin", + "title": "plugin", + "description": "", + "children": [] + }, { "loaded": true, "isPathHref": true, diff --git a/www/apps/resources/sidebar.mjs b/www/apps/resources/sidebar.mjs index 1a415b5be3316..aafc0460ce690 100644 --- a/www/apps/resources/sidebar.mjs +++ b/www/apps/resources/sidebar.mjs @@ -6,6 +6,7 @@ import { currencySidebar } from "./sidebars/currency.mjs" import { customerSidebar } from "./sidebars/customer.mjs" import { fulfillmentSidebar } from "./sidebars/fulfillment.mjs" import { integrationsSidebar } from "./sidebars/integrations.mjs" +import { pluginsSidebar } from "./sidebars/plugins.mjs" import { inventorySidebar } from "./sidebars/inventory.mjs" import { orderSidebar } from "./sidebars/order-module.mjs" import { paymentSidebar } from "./sidebars/payment.mjs" @@ -87,6 +88,13 @@ export const sidebar = sidebarAttachHrefCommonOptions([ isChildSidebar: true, children: integrationsSidebar, }, + { + type: "ref", + path: "/plugins", + title: "Plugins", + isChildSidebar: true, + children: pluginsSidebar, + }, { type: "link", path: "/storefront-development", diff --git a/www/apps/resources/sidebars/plugins.mjs b/www/apps/resources/sidebars/plugins.mjs new file mode 100644 index 0000000000000..890c6426f1a69 --- /dev/null +++ b/www/apps/resources/sidebars/plugins.mjs @@ -0,0 +1,20 @@ +/** @type {import('types').RawSidebarItem[]} */ +export const pluginsSidebar = [ + { + type: "link", + title: "Overview", + path: "/plugins", + }, + { + type: "category", + title: "Guides", + children: [ + { + type: "link", + title: "Wishlist", + path: "/plugins/guides/wishlist", + description: "Learn how to build a wishlist plugin.", + }, + ], + }, +] diff --git a/www/packages/docs-ui/src/components/WorkflowDiagram/List/index.tsx b/www/packages/docs-ui/src/components/WorkflowDiagram/List/index.tsx index 89c89cfff3a43..c2240f3fc470c 100644 --- a/www/packages/docs-ui/src/components/WorkflowDiagram/List/index.tsx +++ b/www/packages/docs-ui/src/components/WorkflowDiagram/List/index.tsx @@ -8,6 +8,7 @@ import { WorkflowDiagramLegend } from "../Common/Legend" export const WorkflowDiagramList = ({ workflow, + hideLegend = false, }: WorkflowDiagramCommonProps) => { const clusters = createNodeClusters(workflow.steps) @@ -20,7 +21,7 @@ export const WorkflowDiagramList = ({ ) })} - + {!hideLegend && } ) } diff --git a/www/packages/docs-ui/src/components/WorkflowDiagram/index.tsx b/www/packages/docs-ui/src/components/WorkflowDiagram/index.tsx index a7948b4b97d84..85384bef100cd 100644 --- a/www/packages/docs-ui/src/components/WorkflowDiagram/index.tsx +++ b/www/packages/docs-ui/src/components/WorkflowDiagram/index.tsx @@ -6,9 +6,13 @@ import { Loading } from "../.." import { WorkflowDiagramCanvas } from "./Canvas" import { WorkflowDiagramList } from "./List" +export type WorkflowDiagramCommonOptionsProps = { + hideLegend?: boolean +} + export type WorkflowDiagramCommonProps = { workflow: Workflow -} +} & WorkflowDiagramCommonOptionsProps export type WorkflowDiagramType = "canvas" | "list"