chore: reorganize docs apps (#7228)
* reorganize docs apps * add README * fix directory * add condition for old docs
This commit is contained in:
1 parent
224ebb2154
commit
4fe28f5a95
6187 files changed
+601447
-598226
No files matched your search
@@ -0,0 +1,37 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} More Resources`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll find more resources to aid you during your development with Medusa.
|
||||
|
||||
## Medusa Resources Documentation
|
||||
|
||||
The Medusa Resources documentation provides guides and references that are useful for your development. This book included links to parts of the Medusa Resources documentation where necessary.
|
||||
|
||||
The following sections highlight other parts of the Medusa Resources documentation that weren’t mentioned before. These are resources that you’re likely to go back to during your development.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Guides
|
||||
|
||||
During your development, you may run into common issues.
|
||||
|
||||
Refer to [this Medusa Resources documentation](!resources!/troubleshooting) for a full list of troubleshooting guides to find a fix.
|
||||
|
||||
---
|
||||
|
||||
## Recipes
|
||||
|
||||
Recipes provide a general overview of how to implement a common use case with Medusa, such as building a marketplace or supporting digital products.
|
||||
|
||||
Refer to [this Medusa Resources documentation](!resources!#recipes) for a full list of available recipes.
|
||||
|
||||
---
|
||||
|
||||
## SDK References
|
||||
|
||||
Medusa provides SDKs and CLI tools to aid you in your development.
|
||||
|
||||
Refer to [this Medusa Resources documentation](!resources!#sdks-and-tools) for a full list of available tools and how to use them.
|
||||
@@ -0,0 +1,85 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Create a Plugin`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to create a plugin.
|
||||
|
||||
## Plugin Development Process
|
||||
|
||||
A plugin consists of Medusa application customizations. So, start with a new Medusa application and build your plugin’s resources.
|
||||
|
||||
Once you’re done developing and testing your customizations, you can package them in a plugin to be installed in other Medusa applications.
|
||||
|
||||

|
||||
|
||||
This chapter explains the steps to create the plugin that packages those customizations. The next chapter explains how to publish the plugin to NPM.
|
||||
|
||||
---
|
||||
|
||||
## Changes to package.json
|
||||
|
||||
### Add Prepare Script
|
||||
|
||||
Your plugin’s customizations must be built for production use. So, add a new `prepare` script to your scripts in `package.json`:
|
||||
|
||||
```json title="package.json"
|
||||
"scripts": {
|
||||
// other scripts...
|
||||
"prepare": "cross-env NODE_ENV=production npm run build:server && medusa-admin bundle"
|
||||
}
|
||||
```
|
||||
|
||||
This builds your application resources, such as services and API routes, and creates a production build of your admin customizations. If your plugin doesn’t have any admin customizations, remove the `medusa-admin bundle` part of the script.
|
||||
|
||||
<Note>
|
||||
|
||||
`cross-env` is already a dependency in your Medusa application. If not, make sure to install it.
|
||||
|
||||
</Note>
|
||||
|
||||
### Change Dependencies
|
||||
|
||||
Your Medusa application requires dependencies that are not relevant to a plugin. For example, you can remove other plugins like `@medusajs/file-local` or modules like `@medusajs/cache-inmemory`.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Make sure to remove those plugins/modules from `medusa-config.js`.
|
||||
|
||||
</Note>
|
||||
|
||||
In addition, move `@medusajs/medusa` to be a peer dependency:
|
||||
|
||||
```json title="package.json"
|
||||
"peerDependencies": {
|
||||
"@medusajs/medusa": "YOUR_MEDUSA_VERSION",
|
||||
// other peer dependencies...
|
||||
}
|
||||
```
|
||||
|
||||
If your plugin includes admin customizations, add `react` and `react-router-dom` (if you’re using it) to the `peerDependencies` as well:
|
||||
|
||||
```json title="package.json"
|
||||
"peerDependencies": {
|
||||
// other dependencies...
|
||||
"react": "^18.2.0",
|
||||
"react-router-dom": "^6.13.0"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Changes
|
||||
|
||||
Your Medusa application has files that aren’t relevant to your plugin. For example, files like `src/model/onboarding.ts` used for the onboarding process.
|
||||
|
||||
Go through all files under the `src` directory and remove those that aren’t relevant to your plugin.
|
||||
|
||||
---
|
||||
|
||||
## Plugin Structure
|
||||
|
||||
A published plugin must have its customizations built into the `dist` directory or to the root of the package. This is handled by the `prepare` command you added earlier.
|
||||
|
||||
Once your plugin is created, you’re ready to publish and use it in Medusa applications.
|
||||
@@ -0,0 +1,102 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Integration Services`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about special service types used when integrating third-party services.
|
||||
|
||||
## Notification Service
|
||||
|
||||
A notification service is used to send notifications, such as email, in your Medusa application.
|
||||
|
||||
For example, the SendGrid plugin contains a notification service. When the plugin is installed, the Medusa application resolves its notification service to send emails using SendGrid.
|
||||
|
||||
A notification service must extend the `AbstractNotificationService` class from the `@medusajs/medusa` package.
|
||||
|
||||
Refer to [this reference](!resources!/notification-service) to learn more about creating a notification service.
|
||||
|
||||
---
|
||||
|
||||
## File Service
|
||||
|
||||
A file service is used to upload and download files in your Medusa application.
|
||||
|
||||
For example, the S3 plugin contains a file service. When the plugin is installed, the Medusa application resolves its file service to upload and download files, such as product images.
|
||||
|
||||
A file service must extend the `AbstractFileService` class from the `@medusajs/medusa` package.
|
||||
|
||||
Refer to [this reference](!resources!/file-service) to learn more about creating a file service.
|
||||
|
||||
---
|
||||
|
||||
## Search Service
|
||||
|
||||
A search service is used to search data in your store, such as products.
|
||||
|
||||
For example, the MeiliSearch plugin contains a search service. When the plugin is installed, the Medusa application resolves its search service when searching for products.
|
||||
|
||||
A search service must extend the `AbstractSearchService` class from the `@medusajs/utils` package.
|
||||
|
||||
Refer to [this reference](!resources!/search-service) to learn more about creating a search service.
|
||||
|
||||
---
|
||||
|
||||
## Other Integration Types
|
||||
|
||||
If you’re integrating other types of services, such as a CMS or an ERP system, you can create a standard service that extends the `TransactionBaseService` class and implements the necessary methods for your integration.
|
||||
|
||||
Then, you can use that service in other resources, such as an API route or a subscriber.
|
||||
|
||||
---
|
||||
|
||||
## Integration Services Tips
|
||||
|
||||
### Client Initialization
|
||||
|
||||
Part of the integration is interacting with the third-party service using a client.
|
||||
|
||||
Some third-party services provide a Node.js SDK that you can use. In those cases, you can install the SDK and initialize the client in the constructor.
|
||||
|
||||
In other cases, you can install [axios](https://www.npmjs.com/package/axios) and use it to create a client in the constructor.
|
||||
|
||||
For example:
|
||||
|
||||
```tsx
|
||||
import { TransactionBaseService } from "@medusajs/medusa"
|
||||
import { Axios } from "axios"
|
||||
|
||||
type Options = {
|
||||
erp_url: string
|
||||
token: string
|
||||
}
|
||||
|
||||
class ErpService extends TransactionBaseService {
|
||||
protected client: Axios
|
||||
|
||||
constructor({}, options: Options) {
|
||||
super(arguments[0], options)
|
||||
|
||||
this.client = new Axios({
|
||||
baseURL: options.erp_url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${options.token}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async retrieveProductErpDetails(productId: string) {
|
||||
const { data } = await this.client.get(
|
||||
`/product/${productId}`
|
||||
)
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
export default ErpService
|
||||
```
|
||||
|
||||
In the above example, you create a client with `axios`. You use the plugin’s options to set the client’s options, such as the base URL and the authorization token.
|
||||
|
||||
Then, you can use the client in other methods to send requests to the third-party service.
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Table } from "docs-ui"
|
||||
import { Check, XMark } from "@medusajs/icons"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Plugin Development`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you’ll learn about plugins, their use in your application, and how to create and publish plugins.
|
||||
|
||||
## What is a Plugin?
|
||||
|
||||
A plugin is an NPM package that consists of Medusa customizations. It adds new functionalities or integrates third-party services, such as a CMS or MeiliSearch.
|
||||
|
||||
Plugins can be added or removed from your Medusa application without causing side effects.
|
||||
|
||||
<Note title="Use plugins when" type="success">
|
||||
|
||||
- You're integrating a third-party service into your Medusa application.
|
||||
- You're creating reusable Medusa application customizations.
|
||||
- You're providing features that are specific to a Medusa application, such as a migration tool from another platform to Medusa.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Plugins vs Modules
|
||||
|
||||
Both plugins and modules are NPM packages that can be installed on your Medusa application without affecting the overall system.
|
||||
|
||||
However, there are key differences between plugins and modules:
|
||||
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell className="w-1/3">Criteria</Table.HeaderCell>
|
||||
<Table.HeaderCell className="!text-center">Plugins</Table.HeaderCell>
|
||||
<Table.HeaderCell className="!text-center">Modules</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.Cell>Run in isolation of a Medusa application.</Table.Cell>
|
||||
<Table.Cell><XMark className="mx-auto" /></Table.Cell>
|
||||
<Table.Cell><Check className="mx-auto" /></Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>Can consist of Medusa application customizations, such as API routes or admin widgets.</Table.Cell>
|
||||
<Table.Cell><Check className="mx-auto" /></Table.Cell>
|
||||
<Table.Cell><XMark className="mx-auto" /></Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>Link custom data models to those of Medusa's commerce modules.</Table.Cell>
|
||||
<Table.Cell><XMark className="mx-auto" /></Table.Cell>
|
||||
<Table.Cell><Check className="mx-auto" /></Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>Make architectural changes, such as change the pub/sub service used to emit events.</Table.Cell>
|
||||
<Table.Cell><XMark className="mx-auto" /></Table.Cell>
|
||||
<Table.Cell><Check className="mx-auto" /></Table.Cell>
|
||||
</Table.Row>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
|
||||
## Official and Community Plugins
|
||||
|
||||
Medusa provides official plugins that integrate third-party services like Algolia, SendGrid, S3, and more.
|
||||
|
||||
You can also check out the [Plugins Library](https://medusajs.com/plugins/) for a full list of plugins created by the community.
|
||||
|
||||
---
|
||||
|
||||
## How are Plugins Installed?
|
||||
|
||||
Plugins are installed through NPM. Then, you must add them to the `plugins` array exported as part of the configurations in `medusa-config.js`.
|
||||
|
||||
If you check the `plugins` array in your `medusa-config.js`, you'll find a few plugins already installed:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
const plugins = [
|
||||
`medusa-fulfillment-manual`,
|
||||
`medusa-payment-manual`,
|
||||
{
|
||||
resolve: `@medusajs/file-local`,
|
||||
options: {
|
||||
upload_dir: "uploads",
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: "@medusajs/admin",
|
||||
/** @type {import('@medusajs/admin').PluginOptions} */
|
||||
options: {
|
||||
autoRebuild: true,
|
||||
develop: {
|
||||
open: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
The items in the `plugins` array can either be:
|
||||
|
||||
- A string which is the plugin's name. In this case, the plugin doesn't require any options;
|
||||
- Or an object having the following properties:
|
||||
- `resolve`: the plugin's name.
|
||||
- `options`: An object of options to pass to the plugin.
|
||||
|
||||
### Configure Admin Customizations
|
||||
|
||||
If a plugin provides customizations to the admin dashboard, you can pass a special option `enableUI` whose value is a boolean indicating whether the admin customizations should be enabled. By default, its value is `false`.
|
||||
|
||||
For example:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
const plugins = [
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-plugin-custom`,
|
||||
options: {
|
||||
// other options...
|
||||
enableUI: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
@@ -0,0 +1,144 @@
|
||||
import { CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Plugin Options`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to access a plugin’s options in different resources.
|
||||
|
||||
## Services
|
||||
|
||||
In a plugin, a service’s constructor accepts a second parameter: an object holding the plugin’s options.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/services/erp.ts" highlights={[["6", "", "The plugin's options are passed as a parameter."], ["9", "", "Set the plugin's options in a class property."]]}
|
||||
import { TransactionBaseService } from "@medusajs/medusa"
|
||||
|
||||
class ErpService extends TransactionBaseService {
|
||||
protected options_: Record<string, unknown>
|
||||
|
||||
constructor(container, options) {
|
||||
super(container, options)
|
||||
|
||||
this.options_ = options
|
||||
}
|
||||
}
|
||||
|
||||
export default ErpService
|
||||
```
|
||||
|
||||
This adds an `options_` property to the `ErpService` and sets its value using the second parameter of the constructor. You can now use the options in other methods.
|
||||
|
||||
---
|
||||
|
||||
## API Routes
|
||||
|
||||
To access a plugin’s options in an API route, it’s recommended to expose a getter method in the service for the `options_` property that the API route can use.
|
||||
|
||||
For example:
|
||||
|
||||
<CodeTabs group="resource-types">
|
||||
<CodeTab label="Service" value="service">
|
||||
|
||||
```ts title="src/services/erp.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]}
|
||||
import { TransactionBaseService } from "@medusajs/medusa"
|
||||
|
||||
class ErpService extends TransactionBaseService {
|
||||
protected options_: Record<string, unknown>
|
||||
// ...
|
||||
|
||||
get options(): Record<string, unknown> {
|
||||
return this.options_
|
||||
}
|
||||
}
|
||||
|
||||
export default ErpService
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="API Route" value="api-route">
|
||||
|
||||
```ts title="src/subscribers/customer-created.ts"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
|
||||
import ErpService from "../../services/erp"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const erpService: ErpService = req.scope.resolve("erpService")
|
||||
|
||||
const pluginOptions = erpService.options
|
||||
|
||||
// use plugin options...
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
---
|
||||
|
||||
## Subscribers
|
||||
|
||||
The object parameter that a subscriber handler function accepts has a `pluginOptions` property. When a subscriber is created in a plugin, this property holds the plugin’s options.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/subscribers/product-updated.ts" highlights={[["7", "", "The plugin's options are passed in the object parameter."]]}
|
||||
import {
|
||||
// other imports...
|
||||
SubscriberArgs,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export default async function productUpdateHandler({
|
||||
pluginOptions,
|
||||
}: SubscriberArgs) {
|
||||
console.log(`Plugin options: ${pluginOptions}`)
|
||||
}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scheduled Job
|
||||
|
||||
The object parameter that a scheduled job function accepts has a `pluginOptions` property. When a scheduled job is created in a plugin, this property holds the plugin’s options.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/jobs/once-a-day.ts" highlights={[["6", "", "The plugin's options are passed in the object parameter."]]}
|
||||
import {
|
||||
// ...
|
||||
ScheduledJobArgs,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export default function ({ pluginOptions }: ScheduledJobArgs) {
|
||||
console.log(`Plugin options: ${pluginOptions}`)
|
||||
}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Loaders
|
||||
|
||||
In a plugin, a loader function accepts a second parameter: an object holding the plugin’s options.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/loaders/on-start.ts" highlights={[["5", "", "The plugin's options are passed as a second parameter to the loader."]]}
|
||||
import { MedusaContainer } from "@medusajs/medusa"
|
||||
|
||||
export default function (
|
||||
container: MedusaContainer,
|
||||
options: Record<string, unknown>
|
||||
) {
|
||||
console.log(`Plugin options: ${options}`)
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,184 @@
|
||||
import { Table } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Publish a Plugin`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to publish a plugin. After you publish your plugin, it will also be available in [Medusa’s Plugins Library](https://medusajs.com/plugins/).
|
||||
|
||||
## package.json Checklist
|
||||
|
||||
Update the following meta information in your plugin’s package.json:
|
||||
|
||||
- `name`: The name of your plugin. By convention, all plugin names start with `medusa` followed by a descriptive name of what the plugin does. For example, `medusa-payment-stripe`.
|
||||
- `description`: A short description of what the plugin does.
|
||||
- `author`: Your name or your company's name.
|
||||
- `repository`: Details about the repository that holds the source code of the plugin. It's an object that holds the following properties:
|
||||
- `type`: Should be `git`.
|
||||
- `url`: The URL to the repository (for example, the GitHub repository holding the code of your plugin).
|
||||
|
||||
### Package Keywords
|
||||
|
||||
It's required for all Medusa plugins to use the keywords `medusa-plugin`.
|
||||
|
||||
The following table mentions other keywords that you can use based on your plugin's functionalities.
|
||||
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Keyword</Table.HeaderCell>
|
||||
<Table.HeaderCell>Description</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.Cell>`medusa-plugin-analytics`</Table.Cell>
|
||||
<Table.Cell>Analytics functionalities or integrations.</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>`medusa-plugin-cms`</Table.Cell>
|
||||
<Table.Cell>CMS functionalities or integrations.</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>`medusa-plugin-notification`</Table.Cell>
|
||||
<Table.Cell>Notification functionalities or integrations.</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>`medusa-plugin-payment`</Table.Cell>
|
||||
<Table.Cell>Payment functionalities or integrations.</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>`medusa-plugin-search`</Table.Cell>
|
||||
<Table.Cell>Search functionalities or integrations.</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>`medusa-plugin-shipping`</Table.Cell>
|
||||
<Table.Cell>Shipping functionalities or integrations.</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>`medusa-plugin-storage`</Table.Cell>
|
||||
<Table.Cell>File service or storage integrations.</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>`medusa-plugin-source`</Table.Cell>
|
||||
<Table.Cell>Migrate or import data into Medusa from another platform.</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>`medusa-plugin-admin`</Table.Cell>
|
||||
<Table.Cell>Plugins that include only admin customizations.</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>`medusa-plugin-other`</Table.Cell>
|
||||
<Table.Cell>Any other type of plugin.</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
|
||||
---
|
||||
|
||||
## Add NPMIgnore File
|
||||
|
||||
Not all files in your Medusa application are relevant to the published plugin.
|
||||
|
||||
Add the following `.npmignore` file in the root of the plugin's directory to ignore unnecessary files from the published plugin:
|
||||
|
||||
```bash title=".npmignore"
|
||||
# directories
|
||||
.cache
|
||||
.DS_store
|
||||
.github
|
||||
.vscode
|
||||
.yarn
|
||||
build
|
||||
data
|
||||
node_modules
|
||||
src
|
||||
uploads
|
||||
|
||||
# files
|
||||
.babelrc
|
||||
.env*
|
||||
.gitignore
|
||||
.eslintrc
|
||||
.prettierrc
|
||||
.yarnrc.yml
|
||||
index.js
|
||||
medusa-config.js
|
||||
yarn.lock
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Publish Plugin
|
||||
|
||||
<Note type="check">
|
||||
|
||||
To publish a plugin, you need an [NPM account](https://www.npmjs.com/signup).
|
||||
|
||||
</Note>
|
||||
|
||||
To publish the plugin:
|
||||
|
||||
1. Run the `prepare` command:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run prepare
|
||||
```
|
||||
|
||||
2. Publish the NPM package:
|
||||
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
If you haven’t logged in before with NPM, you’ll be asked to log in first.
|
||||
|
||||
Your package is then published to NPM.
|
||||
|
||||
---
|
||||
|
||||
## Install Plugin in Medusa Applications
|
||||
|
||||
To install your published plugin in a Medusa application, run the following command:
|
||||
|
||||
```bash npm2yarn
|
||||
npm install medusa-plugin-custom
|
||||
```
|
||||
|
||||
Where `medusa-plugin-custom` is your plugin’s name.
|
||||
|
||||
Then, add the plugin to `medusa-config.js`:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
const plugins = [
|
||||
// ...
|
||||
{
|
||||
resolve: `medusa-plugin-custom`,
|
||||
options: {
|
||||
// plugin options...
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Update Plugin
|
||||
|
||||
To publish plugin changes, first, run the following command in the plugin’s directory to change the NPM version:
|
||||
|
||||
```bash
|
||||
npm version <type>
|
||||
```
|
||||
|
||||
Where `<type>` indicates the type of version update you’re publishing. For example, `major`. You can see the [full list of types in NPM’s documentation](https://docs.npmjs.com/cli/v8/commands/npm-version).
|
||||
|
||||
Then, publish the new update:
|
||||
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
You can then update the plugin in the Medusa applications that use it.
|
||||
@@ -0,0 +1,82 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Create a Batch Job`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to create and process a batch job
|
||||
|
||||
<Note type="check">
|
||||
|
||||
The API routes used in this chapter require admin user authentication, and the authentication token is referred to in code snippets as `<API_TOKEN>`. Refer to [this guide](https://docs.medusajs.com/api/admin#authentication) on authenticating admin users.
|
||||
|
||||
</Note>
|
||||
|
||||
## 1. Start Medusa Application
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 2. Create a Batch Job
|
||||
|
||||
Use the [Create Batch Job API Route](https://docs.medusajs.com/api/admin#batch-jobs_postbatchjobs) to create a new batch job with the same type as the batch job strategy.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://localhost:9000/admin/batch-jobs' \
|
||||
-H 'x-medusa-access-token: <API_TOKEN>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"type": "custom",
|
||||
"context": { }
|
||||
}'
|
||||
```
|
||||
|
||||
In the request body of this API route, you must pass two parameters:
|
||||
|
||||
- `type`: The batch job’s type. You use `custom`, which is the type of batch job strategy created in the previous chapter.
|
||||
- `context`: An object holding any additional details you want to pass to the batch job.
|
||||
|
||||
This API route creates a batch job in the database. The Medusa application then triggers the processing of the batch job asynchronously.
|
||||
|
||||
## 3. Retrieve Batch Job Status
|
||||
|
||||
To check the current status of the batch job, send a request to the [Get a Batch Job API route](https://docs.medusajs.com/api/admin#batch-jobs_getbatchjobsbatchjob):
|
||||
|
||||
```bash
|
||||
curl 'http://localhost:9000/admin/batch-jobs/<BATCH_JOB_ID>' \
|
||||
-H 'x-medusa-access-token: <API_TOKEN>'
|
||||
```
|
||||
|
||||
Replace `<BATCH_JOB_ID>` with the ID of the batch job returned in the previous step.
|
||||
|
||||
This API route returns the batch job object. Among its properties, the `status` property indicates the status of the batch job. If it started processing, the status is `processing`. If it’s completed, the status is `completed`.
|
||||
|
||||
## 4. Check Console
|
||||
|
||||
Once the batch job’s status is `completed`, check the terminal. You’ll find the following messages:
|
||||
|
||||
```bash
|
||||
info: Processing batch.created which has 1 subscribers
|
||||
info: Processing batch.pre_processed which has 0 subscribers
|
||||
info: Processing batch.confirmed which has 1 subscribers
|
||||
Processing the batch job!
|
||||
info: Processing batch.processing which has 0 subscribers
|
||||
info: Processing batch.completed which has 0 subscribers
|
||||
```
|
||||
|
||||
These messages indicate the different stages of the batch job’s lifecycle. Whenever the batch job’s status is changed, an event is triggered to allow batch job strategies to handle them.
|
||||
|
||||
When the batch job’s status is `confirmed`, the batch job is processed, which is why the `Processing the batch job!` message is logged.
|
||||
|
||||
In the next chapter, you’ll learn more about the batch job’s lifecycle.
|
||||
|
||||
---
|
||||
|
||||
## Alternative Method: BatchJobService
|
||||
|
||||
You can alternatively create and process a batch job using the `BatchJobService`.
|
||||
|
||||
Refer to the `BatchJobService` reference to learn how to use the `create` method.
|
||||
@@ -0,0 +1,56 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Batch Job Lifecycles`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about the different lifecycle stages of a batch job based on its status.
|
||||
|
||||
## created
|
||||
|
||||
When a batch job is created through the API route or using the `BatchJobService`, its status is changed to `created` and the `batch.created` event is emitted.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## pre_processed
|
||||
|
||||
Once the `batch.created` event is emitted, the associated batch job strategy is resolved and its `preProcessBatchJob` method is executed.
|
||||
|
||||
Then, the batch job’s status is changed to `pre_processed` and the `batch.pre_processed` event is triggered.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## confirmed
|
||||
|
||||
After the batch job’s status is changed to `pre_processed`, it’s confirmed automatically if the job’s `dry_run` attribute is disabled (which is, by default). The batch job’s status is changed to `confirmed` and the `batch.confirmed` event is triggered.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
If you enabled the `dry_run` attribute when you created the batch job, you must confirm it manually either through the [API route](https://docs.medusajs.com/api/admin#batch-jobs_postbatchjobsbatchjobconfirmprocessing) or the `BatchJobService`'s `confirm` method.
|
||||
|
||||
</Note>
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## processing and completed
|
||||
|
||||
Once the `batch.confirmed` event is emitted, the batch job’s status is changed to `processing` and the `batch.processing` event is emitted. Then, the batch job strategy’s `process` method is executed.
|
||||
|
||||
Once the processing is done, the status of the batch job is changed to `completed` and the `batch.completed` event is emitted.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Other Lifecycle Stages
|
||||
|
||||
The following lifecycle stages occur only in specific cases:
|
||||
|
||||
- `canceled`: If you cancel a batch job using the [API route](https://docs.medusajs.com/api/admin#batch-jobs_postbatchjobsbatchjobcancel) or `BatchJobService`'s `cancel` method, the batch job’s status is changed to `canceled` and the `batch.canceled` event is emitted.
|
||||
- `failed`: If an error occurs during any stage, the batch job’s status is changed to `failed` and the `batch.failed` event is emitted.
|
||||
@@ -0,0 +1,46 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Override Batch Job Strategies`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to override a batch job strategy.
|
||||
|
||||
## Overview
|
||||
|
||||
Medusa defines batch job strategies for different purposes, such as importing products.
|
||||
|
||||
You can override a strategy from the Medusa application’s core to customize its implementation. For example, you can override the product-import strategy to handle a custom file structure or format.
|
||||
|
||||
---
|
||||
|
||||
## How to Override a Batch Job Strategy
|
||||
|
||||
To override a batch job strategy, create a new batch job strategy and set its `batchType` property to the same type as the original batch job strategy.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/strategies/custom-product-import.ts" highlights={[["7", "", "Use the same type as the original batch job strategy."]]}
|
||||
import {
|
||||
AbstractBatchJobStrategy,
|
||||
BatchJobService,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
class CustomImportProductStrategy
|
||||
extends AbstractBatchJobStrategy {
|
||||
static batchType = "product-import"
|
||||
// ...
|
||||
}
|
||||
|
||||
export default CustomImportProductStrategy
|
||||
```
|
||||
|
||||
This creates a new batch job strategy and sets its type to `product-import`.
|
||||
|
||||
Then, when you create a batch job of the same type, the Medusa application resolves and uses your custom batch job strategy instead of the one defined in Medusa.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Refer to the `AbstractBatchJobStrategy` reference for a full list of methods you can implement.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,95 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Batch Jobs`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn what batch jobs are and how to create them.
|
||||
|
||||
## What are Batch Jobs and Strategies?
|
||||
|
||||
A batch job is a task performed asynchronously and iteratively in the background of your Medusa application. The task's implementation is defined in a batch job strategy.
|
||||
|
||||
The Medusa application provides API Routes to create a batch job and tracks its progress. You can also create it using the `BatchJobService`'s `create` method.
|
||||
|
||||
When a batch job is created, the associated batch job strategy is used to process it.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Batch Job Strategy?
|
||||
|
||||
A batch job strategy is a class created in a TypeScript or JavaScript file under the `src/strategies` directory. The class must extend the `AbstractBatchJobStrategy` class from the `@medusajs/medusa` package.
|
||||
|
||||
For example, create the file `src/strategies/custom.ts` with the following content:
|
||||
|
||||
export const highlights = [
|
||||
["8", "", "A unique identifier associated with the strategy."],
|
||||
["9", "", "The type of batch job that this strategy is used for."],
|
||||
["11", "processJob", "Defines the task to perform when the batch job is processed."],
|
||||
["14", "buildTemplate", "This method is only useful if your batch job provides a template file or text that users can download."]
|
||||
]
|
||||
|
||||
```ts title="src/strategies/custom.ts" highlights={highlights}
|
||||
import {
|
||||
AbstractBatchJobStrategy,
|
||||
BatchJobService,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
class CustomJobStrategy extends AbstractBatchJobStrategy {
|
||||
protected batchJobService_: BatchJobService
|
||||
static identifier = "custom-strategy"
|
||||
static batchType = "custom"
|
||||
|
||||
async processJob(batchJobId: string): Promise<void> {
|
||||
console.log("Processing the batch job!")
|
||||
}
|
||||
async buildTemplate(): Promise<string> {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomJobStrategy
|
||||
```
|
||||
|
||||
This creates a batch job strategy implementing the required properties and methods.
|
||||
|
||||
### Properties
|
||||
|
||||
A batch job strategy must implement the following properties:
|
||||
|
||||
- `identifier`: A unique identifier associated with the strategy.
|
||||
- `batchType`: The type of batch job that this strategy is used for. A batch job has a type, and, when it's created, the strategy having that type is used to process the job.
|
||||
|
||||
### Methods
|
||||
|
||||
A batch job strategy must implement the following methods:
|
||||
|
||||
- `processJob`: Defines the task to perform when the batch job is processed.
|
||||
- `buildTemplate`: This method is only useful if your batch job provides a template file or text that users can download. For example, you can return a template CSV file that showcases the accepted CSV format for product import. If your batch job doesn’t support that, you can return an empty string.
|
||||
|
||||
A batch job strategy can also implement methods that run before and after a batch job is processed or when it fails. Refer to the Batch Job Strategy reference for all available methods.
|
||||
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
<Note title="Use batch jobs when" type="success">
|
||||
|
||||
- You’re implementing an asynchronous job that’s triggered manually.
|
||||
- You're tracking the status of the asynchronous job.
|
||||
- You're keeping track of all batch job executions, which are stored in the database.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Don’t use batch jobs if" type="error">
|
||||
|
||||
- You want to trigger the asynchronous job automatically. Instead, use scheduled jobs. You can also create the batch job in a scheduled job.
|
||||
- You want the task to be performed and finished before consecutive tasks. Instead, use a service.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Test Batch Job Strategy
|
||||
|
||||
To test a batch job strategy, use the Admin API Routes to create a batch job of the same type. This is covered in the next chapter.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,27 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Admin Development`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note type="soon">
|
||||
|
||||
Admin customizations are coming soon.
|
||||
|
||||
</Note>
|
||||
|
||||
In the next chapters, you'll learn more about possible admin customizations.
|
||||
|
||||
You can customize the admin dashboard by:
|
||||
|
||||
- Adding new sections to existing pages using Widgets.
|
||||
- Adding new pages using UI Routes.
|
||||
- Adding new pages to the Settings section of the admin dashboard using Setting Pages.
|
||||
|
||||
---
|
||||
|
||||
## Medusa UI Package
|
||||
|
||||
Medusa provides a Medusa UI package to facilitate your admin development through ready-made components and ensure a consistent design between your customizations and the dashboard’s design.
|
||||
|
||||
Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui) to learn how to install it and use its components.
|
||||
@@ -0,0 +1,152 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Admin Setting Pages`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note type="soon">
|
||||
|
||||
Admin customizations are coming soon.
|
||||
|
||||
</Note>
|
||||
|
||||
In this chapter, you’ll learn how to create a setting page in the admin dashboard.
|
||||
|
||||
## What is a Setting Page?
|
||||
|
||||
A setting page is a React component that adds a new page to the settings panel of the admin dashboard.
|
||||
|
||||
For example, you may create a setting page to manage configurations related to your custom functionalities.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Setting Page?
|
||||
|
||||
A setting page is created in a file named `page.tsx` under the `src/admin/settings` directory. The file’s default export must be the React component, and it must also export a configuration object.
|
||||
|
||||
For example, you can create the file `src/admin/settings/custom/page.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/settings/custom/page.tsx"
|
||||
import type { SettingConfig } from "@medusajs/admin"
|
||||
import { ChatBubbleLeftRight } from "@medusajs/icons"
|
||||
|
||||
const CustomSettingPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Custom Setting Page</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const config: SettingConfig = {
|
||||
card: {
|
||||
label: "Custom",
|
||||
description: "Manage your custom settings",
|
||||
// optional
|
||||
icon: ChatBubbleLeftRight,
|
||||
},
|
||||
}
|
||||
|
||||
export default CustomSettingPage
|
||||
```
|
||||
|
||||
The new setting page’s path is the file’s path relative to `src/admin/settings` and prefixed with `/a/settings`. So, the above UI route is a new page added at the path `localhost:7001/a/settings/custom`.
|
||||
|
||||
The setting page is shown on the Settings panel as a card. In the exported configuration object, you configure the card’s label, description, and optionally icon. In this example, you use an icon from [Medusa’s UI Icons package](https://docs.medusajs.com/ui/icons/overview).
|
||||
|
||||
### Test the Setting Page
|
||||
|
||||
To test the setting page, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, open the Settings page of the admin dashboard. You’ll find a new card that links to your setting page.
|
||||
|
||||
---
|
||||
|
||||
## Using UI Components
|
||||
|
||||
Similar to other admin customizations, it’s highly recommended that you use the [Medusa UI package](https://docs.medusajs.com/ui) to match your page’s design with the rest of the Medusa Admin.
|
||||
|
||||
For example, you can rewrite the above UI route to the following:
|
||||
|
||||
```tsx title="src/admin/settings/custom/page.tsx"
|
||||
import type { SettingConfig } from "@medusajs/admin"
|
||||
import { ChatBubbleLeftRight } from "@medusajs/icons"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
|
||||
const CustomSettingPage = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Heading level="h1">Custom Setting Page</Heading>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config: SettingConfig = {
|
||||
card: {
|
||||
label: "Custom",
|
||||
description: "Manage your custom settings",
|
||||
icon: ChatBubbleLeftRight,
|
||||
},
|
||||
}
|
||||
|
||||
export default CustomSettingPage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Setting Page Props
|
||||
|
||||
A setting page receives a `notify` prop, which is an object having the following properties:
|
||||
|
||||
- `success`: a function that shows a success toast message.
|
||||
- `error`: a function that shows an error toast message.
|
||||
- `warn`: a function that shows a warning toast message.
|
||||
- `info`: a function that shows an info toast message.
|
||||
|
||||
Each of these functions accepts two parameters: the message’s title and the message’s content.
|
||||
|
||||
For example:
|
||||
|
||||
```tsx title="src/admin/settings/custom/page.tsx" highlights={[["11", "success", "Show a success toast message on the click of a button."]]}
|
||||
import type { RouteProps, SettingConfig } from "@medusajs/admin"
|
||||
import { ChatBubbleLeftRight } from "@medusajs/icons"
|
||||
import { Container, Heading, Button } from "@medusajs/ui"
|
||||
|
||||
const CustomSettingPage = ({ notify }: RouteProps) => {
|
||||
return (
|
||||
<Container>
|
||||
<Heading level="h1">Custom Setting Page</Heading>
|
||||
<Button
|
||||
onClick={() =>
|
||||
notify.success("Success!", "You clicked the button!")
|
||||
}
|
||||
className="mt-3"
|
||||
>
|
||||
Click me
|
||||
</Button>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config: SettingConfig = {
|
||||
card: {
|
||||
label: "Custom",
|
||||
description: "Manage your custom settings",
|
||||
icon: ChatBubbleLeftRight,
|
||||
},
|
||||
}
|
||||
|
||||
export default CustomSettingPage
|
||||
```
|
||||
|
||||
If you click the button on your setting page, a toast message will show with the title and message you specified.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Admin UI Routes support [Tailwind CSS](https://tailwindcss.com/) out of the box.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,72 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Admin Development Tips`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note type="soon">
|
||||
|
||||
Admin customizations are coming soon.
|
||||
|
||||
</Note>
|
||||
|
||||
In this chapter, you'll find some tips for your admin development.
|
||||
|
||||
## Sending Requests to API Routes
|
||||
|
||||
To send requests to your API routes in your admin customizations, it’s highly recommended to use the Medusa React or Medusa JS Client packages.
|
||||
|
||||
Check out their references for more details on available hooks or methods and how to use them.
|
||||
|
||||
---
|
||||
|
||||
## Routing Functionalities
|
||||
|
||||
To navigate or link to other pages, or use other routing functionalities, use the [react-router-dom](https://reactrouter.com/en/main) package:
|
||||
|
||||
```bash npm2yarn
|
||||
npm install react-router-dom
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx" highlights={[["18", "Link", "Add a link to another page."]]}
|
||||
import type {
|
||||
WidgetConfig,
|
||||
ProductDetailsWidgetProps,
|
||||
} from "@medusajs/admin"
|
||||
import { Container, Heading, Button } from "@medusajs/ui"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
// The widget
|
||||
const ProductWidget = ({
|
||||
product,
|
||||
}: ProductDetailsWidgetProps) => {
|
||||
return (
|
||||
<Container>
|
||||
<Heading level="h2">
|
||||
Product Widget {product.title}
|
||||
</Heading>
|
||||
<Button className="mt-3" variant="transparent">
|
||||
<Link to={"/a/orders"}>View Orders</Link>
|
||||
</Button>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
// The widget's configurations
|
||||
export const config: WidgetConfig = {
|
||||
zone: "product.details.after",
|
||||
}
|
||||
|
||||
export default ProductWidget
|
||||
|
||||
```
|
||||
|
||||
This adds a widget in a product's details page with a link to the Orders page.
|
||||
|
||||
<Note title="Learn more">
|
||||
|
||||
Refer to [react-router-dom’s documentation](https://reactrouter.com/en/main) for other available components and hooks.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,182 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Admin UI Routes`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note type="soon">
|
||||
|
||||
Admin customizations are coming soon.
|
||||
|
||||
</Note>
|
||||
|
||||
In this chapter, you’ll learn how to create a UI route in the admin dashboard.
|
||||
|
||||
## What is a UI Route?
|
||||
|
||||
A UI route is a React Component that adds a custom new page to your admin dashboard. The UI Route can be shown in the sidebar or added as a nested page.
|
||||
|
||||
For example, you may add a new page to manage product reviews.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a UI Route
|
||||
|
||||
A UI route is created in a file named `page.tsx` under the `src/admin/routes` directory. The file’s default export must be the UI route’s React component.
|
||||
|
||||
For example, create the file `src/admin/routes/custom/page.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/routes/custom/page.tsx"
|
||||
const CustomPage = () => {
|
||||
return <div>This is my custom route</div>
|
||||
}
|
||||
|
||||
export default CustomPage
|
||||
```
|
||||
|
||||
The new page’s path is the file’s path relative to `src/admin/routes` and prefixed with `/a`. So, the above UI route is a new page added at the path `localhost:7001/a/custom`.
|
||||
|
||||
### Test the UI Route
|
||||
|
||||
To test the UI route, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, after logging into the admin dashboard, open the page `localhost:7001/a/custom` to see your custom page.
|
||||
|
||||
---
|
||||
|
||||
## Using UI Components
|
||||
|
||||
Similar to Widgets, it’s highly recommended that you use the [Medusa UI package](https://docs.medusajs.com/ui) to match your page’s design with the rest of the Medusa Admin.
|
||||
|
||||
For example, you can rewrite the above UI route to the following:
|
||||
|
||||
```tsx title="src/admin/routes/custom/page.tsx"
|
||||
import { Container } from "@medusajs/ui"
|
||||
|
||||
const CustomPage = () => {
|
||||
return <Container>This is my custom route</Container>
|
||||
}
|
||||
|
||||
export default CustomPage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UI Route Props
|
||||
|
||||
A UI Route receives a `notify` prop, which is an object having the following properties:
|
||||
|
||||
- `success`: a function that shows a success toast message.
|
||||
- `error`: a function that shows an error toast message.
|
||||
- `warn`: a function that shows a warning toast message.
|
||||
- `info`: a function that shows an info toast message.
|
||||
|
||||
Each of these functions accepts two parameters: the message’s title and the message’s content.
|
||||
|
||||
For example:
|
||||
|
||||
```tsx title="src/admin/routes/custom/page.tsx" highlights={[["10", "success", "Show a success toast message on the click of a button."]]}
|
||||
import { RouteProps } from "@medusajs/admin"
|
||||
import { Container, Text, Button } from "@medusajs/ui"
|
||||
|
||||
const CustomPage = ({ notify }: RouteProps) => {
|
||||
return (
|
||||
<Container>
|
||||
<Text>This is my custom route</Text>
|
||||
<Button
|
||||
onClick={() =>
|
||||
notify.success("Success!", "You clicked the button!")
|
||||
}
|
||||
className="mt-3"
|
||||
>
|
||||
Click me
|
||||
</Button>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomPage
|
||||
```
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Admin UI Routes support [Tailwind CSS](https://tailwindcss.com/) out of the box.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Show UI Route in the Sidebar
|
||||
|
||||
A UI route file can export a configuration object that indicates a new item must be added in the sidebar linking to the new UI route.
|
||||
|
||||
The configuration object has the property `link`, which is an object having the following properties:
|
||||
|
||||
- `label`: the new sidebar item’s label.
|
||||
- `icon`: an optional React component that acts as an icon in the sidebar. If not provided, a default icon is used.
|
||||
|
||||
For example:
|
||||
|
||||
```tsx title="src/admin/routes/custom/page.tsx" highlights={[["21"], ["22"], ["23"], ["24"], ["25"], ["26"]]}
|
||||
import { RouteConfig, RouteProps } from "@medusajs/admin"
|
||||
import { Container, Text, Button } from "@medusajs/ui"
|
||||
import { ChatBubbleLeftRight } from "@medusajs/icons"
|
||||
|
||||
const CustomPage = ({ notify }: RouteProps) => {
|
||||
return (
|
||||
<Container>
|
||||
<Text>This is my custom route</Text>
|
||||
<Button
|
||||
onClick={() =>
|
||||
notify.success("Success!", "You clicked the button!")
|
||||
}
|
||||
className="mt-3"
|
||||
>
|
||||
Click me
|
||||
</Button>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config: RouteConfig = {
|
||||
link: {
|
||||
label: "Custom Route",
|
||||
icon: ChatBubbleLeftRight,
|
||||
},
|
||||
}
|
||||
|
||||
export default CustomPage
|
||||
```
|
||||
|
||||
This adds a new sidebar item with the label `Custom Route` and an icon from the [Medusa UI Icons package](https://docs.medusajs.com/ui/icons/overview).
|
||||
|
||||
---
|
||||
|
||||
## Path Parameters
|
||||
|
||||
A UI route can accept path parameters if the name of any of the directories in its path is of the format `[param]`. For example, `src/admin/routes/custom/[id]/page.tsx`.
|
||||
|
||||
To retrieve the path parameters, install the `react-router-dom` to use its `useParams` hook:
|
||||
|
||||
```bash npm2yarn
|
||||
npm install react-router-dom
|
||||
```
|
||||
|
||||
For example, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={[["5", "", "Retrieve the path parameter."], ["7", "{id}", "Show the path parameter."]]}
|
||||
import { useParams } from "react-router-dom"
|
||||
import { Container } from "@medusajs/ui"
|
||||
|
||||
const CustomPage = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
return <Container>Passed ID: {id}</Container>
|
||||
}
|
||||
|
||||
export default CustomPage
|
||||
```
|
||||
@@ -0,0 +1,160 @@
|
||||
import { Table } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Admin Widgets`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
<Note type="soon">
|
||||
|
||||
Admin customizations are coming soon.
|
||||
|
||||
</Note>
|
||||
|
||||
In this chapter, you’ll learn more about widgets and how to use them.
|
||||
|
||||
## What is an Admin Widget?
|
||||
|
||||
Admin widgets are React components you inject into predetermined injection zones in the Medusa Admin dashboard.
|
||||
|
||||
For example, you can add a widget on the order details page that shows payment details retrieved from Stripe.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Widget?
|
||||
|
||||
A widget is created in a file under the `src/admin/widgets` directory. The file’s default export must be the widget, which is the React component, and must also export the widget’s configurations.
|
||||
|
||||
For example, create the file `src/admin/widgets/product-widget.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx" highlights={[["4", "ProductWidget", "The React component of the product widget."], ["14", "", "The zone to inject the widget to."]]}
|
||||
import type { WidgetConfig } from "@medusajs/admin"
|
||||
|
||||
// The widget
|
||||
const ProductWidget = () => {
|
||||
return (
|
||||
<div>
|
||||
<h2>Product Widget</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// The widget's configurations
|
||||
export const config: WidgetConfig = {
|
||||
zone: "product.details.after",
|
||||
}
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
The widget only shows the heading `Product Widget`.
|
||||
|
||||
In the exported widget’s configurations, you must specify the zone to inject the widget into. The `zone` property can be a string or an array of strings, each being the name of the injection zone.
|
||||
|
||||
In the example above, the widget is injected after a product’s details.
|
||||
|
||||
### Test the Widget
|
||||
|
||||
To test out the widget, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, open a product’s details page. You’ll find your custom widget at the bottom of the page.
|
||||
|
||||
---
|
||||
|
||||
## Using UI Components
|
||||
|
||||
It’s highly recommended that you use the [Medusa UI package](https://docs.medusajs.com/ui) to match your widget’s design with the rest of the Medusa Admin.
|
||||
|
||||
For example, you can rewrite the above component to the following:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx"
|
||||
import type { WidgetConfig } from "@medusajs/admin"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
|
||||
const ProductWidget = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Heading level="h2">Product Widget</Heading>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config: WidgetConfig = {
|
||||
zone: "product.details.after",
|
||||
}
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Widget Props
|
||||
|
||||
A widget receives a `notify` prop, which is an object having the following properties:
|
||||
|
||||
- `success`: a function that shows a success toast message.
|
||||
- `error`: a function that shows an error toast message.
|
||||
- `warn`: a function that shows a warning toast message.
|
||||
- `info`: a function that shows an info toast message.
|
||||
|
||||
Each of these functions accepts two parameters: the message’s title, and the message’s content.
|
||||
|
||||
In addition, some injection zones provide additional props based on the page’s context.
|
||||
|
||||
For example, you can rewrite the above widget to the following:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx" highlights={[["14", "product.title", "Show the product's title."], ["18", "success", "Show a success toast message on the click of a button."]]}
|
||||
import type {
|
||||
WidgetConfig,
|
||||
ProductDetailsWidgetProps,
|
||||
} from "@medusajs/admin"
|
||||
import { Container, Heading, Button } from "@medusajs/ui"
|
||||
|
||||
const ProductWidget = ({
|
||||
notify,
|
||||
product,
|
||||
}: ProductDetailsWidgetProps) => {
|
||||
return (
|
||||
<Container>
|
||||
<Heading level="h2">
|
||||
Product Widget {product.title}
|
||||
</Heading>
|
||||
<Button
|
||||
onClick={() =>
|
||||
notify.success("Success!", "You clicked the button!")
|
||||
}
|
||||
className="mt-3"
|
||||
>
|
||||
Click me
|
||||
</Button>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config: WidgetConfig = {
|
||||
zone: "product.details.after",
|
||||
}
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
Since the widget is in the product’s details page, it receives the product as a prop. The widget now shows the title of the product in the header.
|
||||
|
||||
It also shows a button that, when you click, shows a success toast message.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Admin Widgets support [Tailwind CSS](https://tailwindcss.com/) out of the box.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Injection Zone
|
||||
|
||||
Refer to [this reference](!resources!/admin-widget-injection-zones) for the full list of injection zones and their props.
|
||||
@@ -0,0 +1,104 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Handling CORS in API Routes`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes.
|
||||
|
||||
## CORS Overview
|
||||
|
||||
Cross-Origin Resource Sharing (CORS) allows only configured origins to access your API Routes. For example, if you allow only origins starting with `http://localhost:7001` to access your Admin API Routes, other origins accessing those routes get a CORS error.
|
||||
|
||||
### CORS Configurations
|
||||
|
||||
You can configure allowed origins for Store and Admin API Routes using the `store_cors` and `admin_cors` configurations in `medusa-config.js`. Each of these configurations accepts a URL pattern to identify allowed origins.
|
||||
|
||||
For example:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
module.exports = {
|
||||
projectConfig: {
|
||||
admin_cors: "http://localhost:7001",
|
||||
store_cors: "http://localhost:8000",
|
||||
// ...
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
This allows the `http://localhost:7001` origin to access the Admin API Routes, and the `http://localhost:8000` origin to access Store API Routes.
|
||||
|
||||
---
|
||||
|
||||
## CORS in Store and Admin Routes
|
||||
|
||||
Medusa applies the CORS middleware with the specified configurations in `medusa-config.js` on all routes starting with `/store` and `/admin`.
|
||||
|
||||
To disable the CORS middleware for a route, export a `CORS` variable in the route file with its value set to `false`.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/store/custom/route.ts" highlights={[["15"]]}
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const GET = (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: "[GET] Hello world!",
|
||||
})
|
||||
}
|
||||
|
||||
export const CORS = false
|
||||
```
|
||||
|
||||
This disables the CORS middleware on API Routes at the path `/store/custom`.
|
||||
|
||||
---
|
||||
|
||||
## CORS in Custom Routes
|
||||
|
||||
If you create a route that doesn’t start with `/store` or `/admin`, you must apply the CORS middleware manually. Otherwise, all requests to your API route cause a CORS error.
|
||||
|
||||
You can do that in the exported middlewares configurations in `src/api/middlewares.ts`.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [["18", "parseCorsOrigins", "A utility function that parses the CORS configurations in `medusa-config.js`"]]
|
||||
|
||||
```ts title="src/api/middlewares.ts" highlights={highlights}
|
||||
import {
|
||||
ConfigModule,
|
||||
MiddlewaresConfig,
|
||||
} from "@medusajs/medusa"
|
||||
import { parseCorsOrigins } from "medusa-core-utils"
|
||||
import cors from "cors"
|
||||
|
||||
export const config: MiddlewaresConfig = {
|
||||
routes: [
|
||||
{
|
||||
matcher: "/custom*",
|
||||
middlewares: [
|
||||
(req, res, next) => {
|
||||
const configModule: ConfigModule =
|
||||
req.scope.resolve("configModule")
|
||||
|
||||
return cors({
|
||||
origin: parseCorsOrigins(
|
||||
configModule.projectConfig.store_cors
|
||||
),
|
||||
credentials: true,
|
||||
})(req, res, next)
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
This retrieves the configurations exported from `medusa-config.js` and applies the `store_cors` to routes starting with `/custom`.
|
||||
@@ -0,0 +1,47 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} HTTP Methods`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about how to add new API routes for each HTTP method.
|
||||
|
||||
## Handlers of HTTP Methods
|
||||
|
||||
You can define a handler function for each HTTP method in a route file. The function’s name is the name of the HTTP method it handles.
|
||||
|
||||
Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`.
|
||||
|
||||
Creating a route handler function for any of the above HTTP methods exposes a new API route for that method.
|
||||
|
||||
For example, create the file `src/api/store/hello-world/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/store/hello-world/route.ts"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const GET = (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: "[GET] Hello world!",
|
||||
})
|
||||
}
|
||||
|
||||
export const POST = (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: "[POST] Hello world!",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This adds two API Routes:
|
||||
|
||||
- A `GET` route at `localhost:9000/store/hello-world`.
|
||||
- A `POST` route at `localhost:9000/store/hello-world`.
|
||||
@@ -0,0 +1,130 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Middlewares`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about middlewares and how to create them.
|
||||
|
||||
## What is a Middleware?
|
||||
|
||||
A middleware is a function executed when a request is sent to an API Route.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Middleware?
|
||||
|
||||
Middlewares are defined in the special file `src/api/middleware.ts`. The file must export an object of middleware configurations.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/middleware.ts"
|
||||
import { MiddlewaresConfig } from "@medusajs/medusa"
|
||||
|
||||
export const config: MiddlewaresConfig = {
|
||||
routes: [
|
||||
{
|
||||
matcher: "/store*",
|
||||
middlewares: [
|
||||
(req, res, next) => {
|
||||
console.log("Received a request!")
|
||||
|
||||
next()
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
The middleware configurations object has the property `routes`. Its value is an array of middleware route objects, where each object is a middleware to apply to a route pattern.
|
||||
|
||||
In the example above, you define a middleware that logs the message `Received a request!` whenever a request is sent to an API route path starting with `/store`.
|
||||
|
||||
<Note>
|
||||
|
||||
The `matcher` property can be a string or a regular expression.
|
||||
|
||||
</Note>
|
||||
|
||||
### Test Middleware
|
||||
|
||||
To test the middleware, start the application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, send a request to any API route starting with `/store`, such as `localhost:9000/store/products`:
|
||||
|
||||
```bash apiTesting testApiUrl="localhost:9000/store/products" testApiMethod="GET"
|
||||
curl localhost:9000/store/products
|
||||
```
|
||||
|
||||
Once you send the request, you’ll see the following message in the terminal:
|
||||
|
||||
```bash
|
||||
Received a request!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
<Note type="success" title="Use middlewares when">
|
||||
|
||||
- You want to guard API routes by a custom condition.
|
||||
- You're modifying the request body.
|
||||
- You're registering custom resources in the Medusa container.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Middleware Function Parameters
|
||||
|
||||
The middleware function accepts three parameters:
|
||||
|
||||
1. A request object of type `MedusaRequest`.
|
||||
2. A response object of type `MedusaResponse`.
|
||||
3. A function of type `MedusaNextFunction` that executes the next middleware in the stack.
|
||||
|
||||
<Note title="Important">
|
||||
|
||||
You must call the `next` function in the middleware. Otherwise, other middlewares and the API route handler won’t execute.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Restrict HTTP Methods
|
||||
|
||||
In addition to the `matcher` configuration, you can restrict which HTTP methods the middleware is applied to.
|
||||
|
||||
The object in the `routes` array accepts the property `method` whose value is either a string or an array of strings. Each string is an allowed HTTP method. If no value is specified, the middlewares are applied to requests of all HTTP methods.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [["7", "", "Apply the middleware only on `POST` requests"]]
|
||||
|
||||
```ts title="src/api/middlewares.ts" highlights={highlights}
|
||||
import { MiddlewaresConfig } from "@medusajs/medusa"
|
||||
|
||||
export const config: MiddlewaresConfig = {
|
||||
routes: [
|
||||
{
|
||||
matcher: "/store*",
|
||||
method: "POST",
|
||||
middlewares: [
|
||||
(req, res, next) => {
|
||||
console.log("Received a request!")
|
||||
|
||||
next()
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
This applies the middleware only when a `POST` request is sent to an API route path starting with `/store`.
|
||||
@@ -0,0 +1,146 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} API Route Parameters`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about path, query, and request body parameters.
|
||||
|
||||
## Path Parameters
|
||||
|
||||
To define a path parameter for an API route, create a directory as part of the route file’s path. The directory’s name is of the format `[param]`, where `param` is the name of the parameters.
|
||||
|
||||
For example, to create an API Route at the path `/message/{id}`, where `{id}` is an ID that can be passed to the route, create the file `src/api/store/hello-world/[id]/route.ts` with the following content:
|
||||
|
||||
export const singlePathHighlights = [
|
||||
["11", "req.params.id", "Access the path parameter `id`"]
|
||||
]
|
||||
|
||||
```ts title="src/api/store/hello-world/[id]/route.ts" highlights={singlePathHighlights}
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const GET = (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: `[GET] Hello ${req.params.id}!`,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
You can access the path parameter using the `params` property of the `MedusaRequest` parameter. The `params` property is an object whose keys are the parameter names, and the values are each parameter’s value passed in the route’s path.
|
||||
|
||||
### Multiple Path Parameters
|
||||
|
||||
Each directory in the route file’s path whose name is of the format `[param]` is considered a path parameter. However, every parameter name must be unique.
|
||||
|
||||
For example, you can create an API route at `src/api/store/hello-world/[id]/name/[name]/route.ts`:
|
||||
|
||||
export const multiplePathHighlights = [
|
||||
["11", "req.params.id", "Access the path parameter `id`"],
|
||||
["11", "req.params.name", "Access the path parameter `name`"]
|
||||
]
|
||||
|
||||
```ts title="src/api/store/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights}
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const GET = (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: `[GET] Hello ${req.params.id} - ${req.params.name}!`,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This API route expects two path parameters: `id` and `name`.
|
||||
|
||||
---
|
||||
|
||||
## Query Parameters
|
||||
|
||||
You can access all query parameters in the `query` property of the `MedusaRequest` parameter.
|
||||
|
||||
For example:
|
||||
|
||||
export const queryHighlights = [
|
||||
["11", "req.query.name", "Access the query parameter `name`"],
|
||||
]
|
||||
|
||||
```ts title="src/api/store/hello-world/route.ts" highlights={queryHighlights}
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
res.json({
|
||||
message: `Hello ${req.query.name}`,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Request Body Parameters
|
||||
|
||||
By default, any request sent to your Medusa application with its `Content-Type` header set to `application/json` is parsed into an object and attached to the `MedusaRequest`'s `body` property.
|
||||
|
||||
For example:
|
||||
|
||||
export const bodyHighlights = [
|
||||
["15", "req.body.name", "Access the request body parameter `name`"],
|
||||
]
|
||||
|
||||
```ts title="src/api/store/hello-world/route.ts" highlights={bodyHighlights}
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
type HelloWorldReq = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export const POST = (
|
||||
req: MedusaRequest<HelloWorldReq>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: `[POST] Hello ${req.body.name}!`,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The `MedusaRequest` type accepts a type argument indicating the expected request body parameters.
|
||||
|
||||
In this example, you use the `name` request body parameter to create the message in the returned response.
|
||||
|
||||
To test it out, send the following request to your Medusa application:
|
||||
|
||||
```bash apiTesting testApiUrl="http://localhost:9000/store/hello-world" testApiMethod="POST" testBodyParams={{ "name": "John" }}
|
||||
curl -X POST http://localhost:9000/store/hello-world \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"name": "John"
|
||||
}'
|
||||
```
|
||||
|
||||
This returns the following JSON object:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "[POST] Hello John!"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,155 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Protected Routes`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to create protected routes.
|
||||
|
||||
## Default Protected Routes
|
||||
|
||||
A protected route is a route that requires requests to be user-authenticated before performing the route's functionality. Otherwise, the request fails, and the user is prevented access.
|
||||
|
||||
Medusa applies an authentication guard on the following routes:
|
||||
|
||||
- Routes starting with `/admin` require an authenticated admin user.
|
||||
- Routes starting with `/store/me` require an authenticated customer.
|
||||
|
||||
<Note>
|
||||
|
||||
Refer to the API Reference for [Admin](https://docs.medusajs.com/api/admin#authentication) and [Store](https://docs.medusajs.com/api/store#authentication) authentication methods.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Authentication Opt-Out
|
||||
|
||||
To disable the authentication guard on custom routes under the `/admin` or `/store/me` path prefixes, export an `AUTHENTICATE` variable in the route file with its value set to `false`.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/store/me/custom/route.ts" highlights={[["15"]]} apiTesting testApiUrl="http://localhost:9000/store/me/custom" testApiMethod="GET"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: "Hello",
|
||||
})
|
||||
}
|
||||
|
||||
export const AUTHENTICATE = false
|
||||
```
|
||||
|
||||
Now, any request sent to the `/store/me/custom` API route is allowed, regardless if the customer is authenticated or not.
|
||||
|
||||
---
|
||||
|
||||
## Access Logged-In Customer
|
||||
|
||||
You can access the logged-in customer’s ID in all API routes starting with `/store` using the `user.customer_id` property of the `MedusaRequest` object.
|
||||
|
||||
For example:
|
||||
|
||||
|
||||
|
||||
```ts title="src/api/store/custom/route.ts" highlights={[["16", "", "Access the logged-in customer's ID."]]}
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { ICustomerModuleService } from "@medusajs/types"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const customerService: ICustomerModuleService =
|
||||
req.scope.resolve(ModuleRegistrationName.CUSTOMER)
|
||||
|
||||
const customer = await customerService.retrieve(
|
||||
req.user.customer_id
|
||||
)
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
In the route handler, you resolve the `CustomerService`, then use it to retrieve the logged-in customer, if available.
|
||||
|
||||
---
|
||||
|
||||
## Access Logged-In Admin User
|
||||
|
||||
You can access the logged-in admin user’s ID in all API Routes starting with `/admin` using the `user.userId` property of the `MedusaRequest` object.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/admin/custom/route.ts" highlights={[["16", "req.user.userId", "Access the logged-in admin user's ID."]]}
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IUserModuleService } from "@medusajs/types"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const userService: IUserModuleService = req.scope.resolve(
|
||||
ModuleRegistrationName.USER
|
||||
)
|
||||
|
||||
const user = await userService.retrieve(req.user.userId)
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
In the route handler, you resolve the `UserService`, and then use it to retrieve the logged-in admin user.
|
||||
|
||||
---
|
||||
|
||||
## Protect Custom API Routes
|
||||
|
||||
To protect custom API Routes that don’t start with `/store/me` or `/admin`, apply one of the following middlewares exported by the `@medusajs/medusa` package on your routes:
|
||||
|
||||
- `authenticate`: only authenticated admin users can access the API Route. You can access the user's ID in the API Route method handler using the `MedusaRequest` object's `user.userId`.
|
||||
- `authenticateCustomer`: customer authentication isn’t required, but if a customer is logged in, it attaches their ID to the `MedusaRequest` object's `user.customer_id`.
|
||||
- `requireCustomerAuthentication`: only authenticated customers can access the API Route. You can access the customer's ID in the API Route method handler using the `MedusaRequest` object's `user.customer_id`.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["11", "authenticate", "Only authenticated admin users can access routes starting with `/custom/admin`"],
|
||||
["15", "requireCustomerAuthentication", "Only authenticated customers can access routes starting with `/custom/customers`"]
|
||||
]
|
||||
|
||||
```ts title="src/api/middlewares.ts" highlights={highlights}
|
||||
import {
|
||||
authenticate,
|
||||
requireCustomerAuthentication,
|
||||
type MiddlewaresConfig,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const config: MiddlewaresConfig = {
|
||||
routes: [
|
||||
{
|
||||
matcher: "/custom/admin*",
|
||||
middlewares: [authenticate()],
|
||||
},
|
||||
{
|
||||
matcher: "/custom/customer*",
|
||||
middlewares: [requireCustomerAuthentication()],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,66 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Request Body Parsers`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to configure request-body parsing and add new parsers.
|
||||
|
||||
## Add a Request-Body Parser
|
||||
|
||||
You can parse request bodies of other content types by adding the parser as a middleware to the routes.
|
||||
|
||||
For example:
|
||||
|
||||
export const parserHighlights = [
|
||||
["13", "", "Add a parser for the `application/x-www-form-urlencoded` content type."]
|
||||
]
|
||||
|
||||
```ts title="src/api/middlewares.ts" highlights={parserHighlights}
|
||||
import type {
|
||||
MiddlewaresConfig,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
urlencoded,
|
||||
} from "body-parser"
|
||||
|
||||
export const config: MiddlewaresConfig = {
|
||||
routes: [
|
||||
{
|
||||
matcher: "*",
|
||||
middlewares: [
|
||||
urlencoded({ extended: true }),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
This adds a parser for the `application/x-www-form-urlencoded` content type and attaches the parsed data to the `MedusaRequest` object’s `body` property.
|
||||
|
||||
---
|
||||
|
||||
## Parse Webhook Body Parameters
|
||||
|
||||
Webhook API Routes may require the `raw` body parser middleware rather than the default `json`.
|
||||
|
||||
To change the default parser, set the `bodyParser` property of a middleware route object to `false`, and pass the preferred body-parser middleware in the `middlewares` property.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/middlewares.ts" highlights={[["8", "", "Disables the default request-body parser."], ["9", "raw", "Add a new body parser."]]}
|
||||
import { MiddlewaresConfig } from "@medusajs/medusa"
|
||||
import { raw } from "body-parser"
|
||||
|
||||
export const config: MiddlewaresConfig = {
|
||||
routes: [
|
||||
{
|
||||
matcher: "/webhooks*",
|
||||
bodyParser: false,
|
||||
middlewares: [raw({ type: "application/json" })],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
This changes the request-body parser to use the `raw` middleware on routes starting with `/webhooks`.
|
||||
@@ -0,0 +1,102 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Common Data Model Definitions`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn where to find resources on common data model definitions related to field's column types, relations, and indices.
|
||||
|
||||
## MikroORM Learning Resources
|
||||
|
||||
Refer to the following resources to learn about common definitions:
|
||||
|
||||
1. [Field column types](https://mikro-orm.io/docs/defining-entities).
|
||||
2. [Creating indices](https://mikro-orm.io/docs/defining-entities#indexes).
|
||||
|
||||
Refer to MikroORM's documentation for more details related to your use case.
|
||||
|
||||
---
|
||||
|
||||
## Relations Between Data Models
|
||||
|
||||
You can build relations between data models in the same module.
|
||||
|
||||
Refer to [MikroORM's relations definition](https://mikro-orm.io/docs/relationships)
|
||||
|
||||
<Note>
|
||||
|
||||
For building relationships between data models in different module, refer to the [Module Relationships chapter](../../modules/module-relationships/page.mdx) instead.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Example Data Model
|
||||
|
||||
The following example showcase a data model with common definitions:
|
||||
|
||||
```ts
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
Enum,
|
||||
OneToOne,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import {
|
||||
createPsqlIndexStatementHelper,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
import ProductVariant from "./product-variant"
|
||||
|
||||
export enum MediaType {
|
||||
MAIN = "main",
|
||||
PREVIEW = "preview"
|
||||
}
|
||||
|
||||
const VariantIdIndex = createPsqlIndexStatementHelper({
|
||||
name: "IDX_product_media_variant_id",
|
||||
tableName: "product_media",
|
||||
columns: "variant_id",
|
||||
}).MikroORMIndex
|
||||
|
||||
@Entity({ tableName: "product_media" })
|
||||
export default class ProductMedia {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
name: string
|
||||
|
||||
@Enum({ items: ["main", "preview"] })
|
||||
type: MediaType
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
file_key: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
mime_type: string
|
||||
|
||||
@VariantIdIndex()
|
||||
@Property({ columnType: "text" })
|
||||
variant_id: string
|
||||
|
||||
@OneToOne({
|
||||
entity: ProductVariant,
|
||||
onDelete: "cascade",
|
||||
})
|
||||
variant: ProductVariant
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "promed")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the example above:
|
||||
|
||||
- The `ProductMedia` data model has the columns `id`, `name`, `type`, `file_key`, `mime_type`, and `variant_id`.
|
||||
- The data model has an index on the `variant_id` column.
|
||||
- The data model has a one-to-one relation to the `ProductVariant` data model.
|
||||
@@ -0,0 +1,75 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Soft-Deletable Models`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this document, you'll learn how to create soft-deletable data models.
|
||||
|
||||
## What is a Soft-Deletable Model?
|
||||
|
||||
A soft-deletable data model is a model whose records aren't actually removed from the database when they're deleted.
|
||||
|
||||
Instead, their `deleted_at` field is set to the date the record was deleted.
|
||||
|
||||
When retrieving or listing records of that data model, records having their `deleted_at` field set aren't retrieved unless the `withDeleted` filter is provided.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Soft-Deletable Model?
|
||||
|
||||
To create a soft-deletable model, first, add the following filter decorator to the data model class:
|
||||
|
||||
```ts title="src/module/hello/models/my-soft-deletable.ts" highlights={[["6"]]}
|
||||
// other imports...
|
||||
import { Entity, Filter } from "@mikro-orm/core"
|
||||
import { DALUtils } from "@medusajs/utils"
|
||||
|
||||
@Entity()
|
||||
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
|
||||
export default class MySoftDeletable {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Then, add a `deleted_at` field to the data model:
|
||||
|
||||
```ts highlights={[["4"], ["5"]]}
|
||||
// other imports...
|
||||
import { Property } from "@mikro-orm/core"
|
||||
|
||||
export default class MySoftDeletable {
|
||||
// ...
|
||||
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date | null = null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Managing Soft-Deletable Models
|
||||
|
||||
Services extending the service factory have methods to soft delete and restore records for all models specified during its creation.
|
||||
|
||||
For example, to soft delete a `MySoftDeletable` record:
|
||||
|
||||
```ts
|
||||
await helloModuleService.softDelete([
|
||||
"id_123", "id_321",
|
||||
])
|
||||
```
|
||||
|
||||
The method receives an array of IDs of records to delete.
|
||||
|
||||
To restore a `MySoftDeletable` record:
|
||||
|
||||
```ts
|
||||
await helloModuleService.restore([
|
||||
"id_123", "id_321",
|
||||
])
|
||||
```
|
||||
|
||||
The method also receives an array of IDs of records to restore.
|
||||
|
||||
If the data model isn't the main data model, its method names are `softDelete` and `restore` suffixed with the plural name of the model. For example, `softDeleteMySoftDeletable`.
|
||||
@@ -0,0 +1,44 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Event Data Payload`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how subscribers receive an event's data payload.
|
||||
|
||||
## How to Access Data Payload
|
||||
|
||||
When events are emitted, they’re emitted with a data payload.
|
||||
|
||||
The object that the subscriber function receives as a parameter has a `data` property, which is the event's data payload.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["8", "", "The event's data payload."],
|
||||
["9", "{ id: string }", "The type of expected data payloads."]
|
||||
]
|
||||
|
||||
```ts title="src/subscribers/product-created.ts" highlights={highlights}
|
||||
import {
|
||||
ProductService,
|
||||
SubscriberArgs,
|
||||
type SubscriberConfig,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export default async function productCreateHandler({
|
||||
data,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
console.log(`The product ${data.id} was created`)
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: ProductService.Events.CREATED,
|
||||
}
|
||||
```
|
||||
|
||||
This logs the product ID received in the `ProductService.Events.CREATED` (`product-created`) event’s data payload to the console.
|
||||
|
||||
## List of Events with Data Payload
|
||||
|
||||
Refer to [this reference](!resources!/events-reference) for a full list of events emitted by Medusa and their data payloads.
|
||||
@@ -0,0 +1,90 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Loaders Outside Modules`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to create a loader outside a module.
|
||||
|
||||
## Loaders in the Medusa Application
|
||||
|
||||
In your Medusa application, you can create loaders under the `src/loaders` directory. The Medusa application runs these loaders on start-up.
|
||||
|
||||
This is useful if you’re performing a task on application start-up, but don’t need to define it in a module.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Loader Outside Module?
|
||||
|
||||
To create a loader in your Medusa application outside a module, create a TypeScript or JavaScript file under the `src/loaders` directory that default exports a function.
|
||||
|
||||
For example, create the file `src/loaders/hello-world.ts` with the following content:
|
||||
|
||||
```ts title="src/loaders/hello-world.ts"
|
||||
export default function () {
|
||||
console.log(
|
||||
"[HELLO LOADER] Just started the Medusa application!"
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
This loader logs the message `[HELLO LOADER] Just started the Medusa application!` when the Medusa application starts.
|
||||
|
||||
### Test Out the Loader
|
||||
|
||||
To test out your loader, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
You’ll see in the terminal the logged message, indicating that the loader function was executed successfully.
|
||||
|
||||
---
|
||||
|
||||
## Resolve Resources
|
||||
|
||||
When the loader function is created outside a module, it receives the Medusa container as a parameter. Use it to resolve other resources in your application, such as a module’s service.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/loaders/hello-world.ts"
|
||||
import { MedusaContainer } from "@medusajs/medusa"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
|
||||
export default async function (container: MedusaContainer) {
|
||||
const productModuleService: IProductModuleService =
|
||||
container.resolve(ModuleRegistrationName.PRODUCT)
|
||||
|
||||
const [, count] = await productModuleService.listAndCount()
|
||||
|
||||
console.log(`Hello! You have ${count} product(s)`)
|
||||
}
|
||||
```
|
||||
|
||||
In the loader you resolve the `IProductModuleService`, then use its `listAndCount` method to log in the terminal the number of products in the store.
|
||||
|
||||
---
|
||||
|
||||
## Medusa Configuration Parameter
|
||||
|
||||
When the loader function is created outside a module, it receives the Medusa configurations defined in `medusa-config.js` as a second parameter.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/loaders/hello-world.ts"
|
||||
import { MedusaContainer } from "@medusajs/medusa"
|
||||
import { ConfigModule } from "@medusajs/types"
|
||||
|
||||
export default async function (
|
||||
container: MedusaContainer,
|
||||
config: ConfigModule
|
||||
) {
|
||||
console.log(`You have ${
|
||||
Object.values(config.modules || {}).length
|
||||
} modules!`)
|
||||
}
|
||||
```
|
||||
|
||||
This loader logs on application start-up the number of modules defined in your Medusa configurations.
|
||||
@@ -0,0 +1,56 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Database Connection Loader`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this document, you’ll learn about how to load the database connection in your module.
|
||||
|
||||
## Modules Database Connection
|
||||
|
||||
By default, a module will use the same database connection of the Medusa application.
|
||||
|
||||
To access those database credentials and ensure that MikroORM’s [entity](https://mikro-orm.io/docs/entity-manager) and [transaction](https://mikro-orm.io/docs/transactions) managers use those credentials when performing database operations, you must pass a database connection loader in your module’s definition.
|
||||
|
||||
Medusa provides a utility connection loader factory that handles registering the database options and entity and transaction managers into the module’s container.
|
||||
|
||||
---
|
||||
|
||||
## How to Add a Database Connection Loader?
|
||||
|
||||
In your module’s definition file, import the `ModulesSdkUtils` utility function from `@medusajs/utils`. Then, use its `mikroOrmConnectionLoaderFactory` function to create a connection loader and export it in the module’s definition.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/modules/hello/index.ts"
|
||||
// other imports...
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
|
||||
const pathToMigrations = __dirname + "/migrations"
|
||||
|
||||
// ...
|
||||
|
||||
const connectionLoader = ModulesSdkUtils
|
||||
.mikroOrmConnectionLoaderFactory({
|
||||
moduleName,
|
||||
moduleModels: [MyCustom],
|
||||
migrationsPath: pathToMigrations,
|
||||
})
|
||||
|
||||
export default {
|
||||
// ...
|
||||
loaders: [
|
||||
// ...
|
||||
connectionLoader,
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
The `mikroOrmConnectionLoaderFactory` function accepts an object with the following properties:
|
||||
|
||||
- `moduleName`: The name of the module.
|
||||
- `moduleModules`: An array of data models in the module.
|
||||
- `migrationsPath`: The path to the migrations file.
|
||||
|
||||
You can now implement data-management methods as explained in the next chapters.
|
||||
@@ -0,0 +1,118 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Module's Container`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about a module's container and how to register resources in that container.
|
||||
|
||||
## Module's Container
|
||||
|
||||
Each module has a local container only used by the resources of that module.
|
||||
|
||||
So, resources in the module, such as services or loaders, can only resolve resources registered in the module's container.
|
||||
|
||||
To register a module's resources in that container, Medusa provides a utility loader factory function that registers your module's services and other resources in the module's container.
|
||||
|
||||
---
|
||||
|
||||
## How to Register a Modules Resources
|
||||
|
||||
In your module's definition file, import the `ModulesSdkUtils` utility functions from the `@medusajs/utils`. Then, use its `moduleContainerLoaderFactory` function to create the container loader and export it in the module's definition.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/modules/hello/index.ts"
|
||||
// other imports...
|
||||
import HelloModuleService from "./service"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
import {
|
||||
ModulesSdkUtils,
|
||||
MikroOrmBaseRepository,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
// ...
|
||||
|
||||
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
|
||||
moduleModels: {
|
||||
MyCustom,
|
||||
},
|
||||
moduleRepositories: {
|
||||
BaseRepository: MikroOrmBaseRepository,
|
||||
},
|
||||
moduleServices: {
|
||||
HelloModuleService,
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
// ...
|
||||
loaders: [containerLoader],
|
||||
}
|
||||
```
|
||||
|
||||
### Container Loader Factory Parameters
|
||||
|
||||
The `moduleContainerLoaderFactory` function accepts as a parameter an object with the following properties:
|
||||
|
||||
- `moduleModels`: An object of the module's data models, where the key is the model's name and the value is the model class.
|
||||
- `moduleRepositories`: An object of the module's repositories. You import here the `MikroOrmBaseRepository` from `@medusajs/utils` and use it as the value of `BaseRepository`.
|
||||
- `moduleServices`: An object of the module's services, where the key is the service's registration name and the value is the service class.
|
||||
|
||||
### Resources Registered by the Container Loader
|
||||
|
||||
The container loader registers in the module's container:
|
||||
|
||||
- The services passed in the `moduleServices` property.
|
||||
- The repositories provided in the `moduleRepositories`.
|
||||
- A generated service for each data model provided in `moduleModels`.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Resolve Resources
|
||||
|
||||
### Services
|
||||
|
||||
A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container.
|
||||
|
||||
For example, to resolve the `baseRepository` registered by the `containerLoader` in the module's definition:
|
||||
|
||||
```ts
|
||||
import { DAL } from "@medusajs/types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
export default class HelloModuleService {
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
constructor({ baseRepository }: InjectedDependencies) {
|
||||
this.baseRepository_ = baseRepository
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Loader
|
||||
|
||||
A loader function in a module accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import {
|
||||
LoaderOptions,
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
|
||||
export default function helloWorldLoader({
|
||||
container,
|
||||
}: LoaderOptions) {
|
||||
const myCustomService = container.resolve("myCustomService")
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Database Operations in Service Methods`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this document, you’ll learn how to implement database operations, such as creating a record, in a service’s methods.
|
||||
|
||||
## Transaction Manager
|
||||
|
||||
A transaction wraps a set of database operations to ensure that when an error occurs, all database changes made by the executed operations are rolled back.
|
||||
|
||||
When you implement a method in your service, you must add the `@InjectTransactionManager` decorator to the method performing database operations. For example:
|
||||
|
||||
```ts title="src/modules/hello/service.ts" highlights={[["26"]]}
|
||||
// other imports
|
||||
import { InjectTransactionManager } from "@medusajs/utils"
|
||||
import { DAL } from "@medusajs/types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
myCustomService: ModulesSdkTypes.InternalModuleService<any>
|
||||
}
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory<
|
||||
// ...
|
||||
>(
|
||||
// ...
|
||||
) {
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
|
||||
constructor(
|
||||
{ baseRepository }: InjectedDependencies
|
||||
) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.baseRepository_ = baseRepository
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async create(
|
||||
// TODO add parameters
|
||||
) {
|
||||
// TODO add implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `@InjectTransactionManager` decorator accepts as a parameter the name of the service class field to inject the transaction manager to, which is the `baseRepository_` field.
|
||||
|
||||
---
|
||||
|
||||
## Generated Service Methods
|
||||
|
||||
As mentioned in a previous chapter, when you use the container loader factory to register resources in your module’s container, the factory creates a service for each data model you specify and registers it in the container. The registration name is the camel-case name of the data model with `Service` appended to it.
|
||||
|
||||
So, the container loader factory creates a `myCustomService` for the `MyCustom` data model.
|
||||
|
||||
These generated services already implement data-management methods such as `create` or `update`. So, in your module’s service, you can resolve the generated service and use it to create a record.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["13", "", "Inject myCustomService, which is the service generated by the container loader for the MyCustom data model."],
|
||||
["23", "", "Add a new field for the generated service of the MyCustom data model."],
|
||||
["31", "", "Set the class field to the injected dependency."],
|
||||
["39", "create", "Use the `create` method of the generated service."]
|
||||
]
|
||||
|
||||
```ts title="src/modules/hello/service.ts" highlights={highlights}
|
||||
// other imports...
|
||||
import { ModulesSdkTypes } from "@medusajs/types"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
|
||||
// ...
|
||||
|
||||
type CreateMyCustomDTO = {
|
||||
name: string
|
||||
}
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
myCustomService: ModulesSdkTypes.InternalModuleService<any>
|
||||
}
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory<
|
||||
// ...
|
||||
>(
|
||||
// ...
|
||||
) {
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
protected myCustomService_: ModulesSdkTypes.InternalModuleService<MyCustom>
|
||||
|
||||
constructor(
|
||||
{ baseRepository, myCustomService }: InjectedDependencies
|
||||
) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.baseRepository_ = baseRepository
|
||||
this.myCustomService_ = myCustomService
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async create(
|
||||
data: CreateMyCustomDTO,
|
||||
@MedusaContext() context: Context = {}
|
||||
): Promise<MyCustomDTO> {
|
||||
const myCustom = await this.myCustomService_.create(
|
||||
data,
|
||||
context
|
||||
)
|
||||
|
||||
return myCustom
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, you add `myCustomService` to the `InjectedDependencies` type to resolve the service in the constructor and set it in a class field.
|
||||
|
||||
In the `create` method, you use `myCustomService`'s `create` method to create the record. You pass it the record’s data as a first parameter, and the Medusa application’s context as a second parameter.
|
||||
@@ -0,0 +1,411 @@
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent, TabsContentWrapper, Badge } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Example: CRUD Module Service`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll find an example of how to implement CRUD operations in your module’s service and test it out with an API route.
|
||||
|
||||
<Note>
|
||||
|
||||
The example in this chapter uses the same `hello` module with the `HelloModuleService` and `MyCustom` data model from previous chapters.
|
||||
|
||||
</Note>
|
||||
|
||||
## 1. Create Types File
|
||||
|
||||
Create the file `src/types/hello/my-custom.ts` with the following content:
|
||||
|
||||
```ts title="src/types/hello/my-custom.ts"
|
||||
export type MyCustomDTO = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type CreateMyCustomDTO = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type UpdateMyCustomDTO = {
|
||||
name?: string
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
You’ll be using these types in the service.
|
||||
|
||||
---
|
||||
|
||||
## 2. Create HelloModuleService
|
||||
|
||||
Create or change the service at `src/modules/hello/service.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/service.ts"
|
||||
import {
|
||||
ModulesSdkUtils,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
DAL,
|
||||
ModulesSdkTypes,
|
||||
Context,
|
||||
} from "@medusajs/types"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
import {
|
||||
CreateMyCustomDTO,
|
||||
MyCustomDTO,
|
||||
UpdateMyCustomDTO,
|
||||
} from "../../types/hello/my-custom"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
myCustomService: ModulesSdkTypes.InternalModuleService<any>
|
||||
}
|
||||
|
||||
type AllModelsDTO = {
|
||||
MyCustom: {
|
||||
dto: MyCustomDTO
|
||||
}
|
||||
}
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory<
|
||||
InjectedDependencies,
|
||||
MyCustomDTO,
|
||||
AllModelsDTO
|
||||
>(MyCustom, []) {
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
protected myCustomService_: ModulesSdkTypes.InternalModuleService<MyCustom>
|
||||
|
||||
constructor(
|
||||
{ baseRepository, myCustomService }: InjectedDependencies
|
||||
) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.baseRepository_ = baseRepository
|
||||
this.myCustomService_ = myCustomService
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async create(
|
||||
data: CreateMyCustomDTO,
|
||||
@MedusaContext() context: Context = {}
|
||||
): Promise<MyCustomDTO> {
|
||||
const myCustom = await this.myCustomService_.create(
|
||||
data,
|
||||
context
|
||||
)
|
||||
|
||||
return myCustom
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async update(
|
||||
id: string,
|
||||
data: UpdateMyCustomDTO,
|
||||
@MedusaContext() context: Context = {}
|
||||
): Promise<MyCustomDTO> {
|
||||
const myCustom = await this.myCustomService_.update({
|
||||
...data,
|
||||
id,
|
||||
})
|
||||
|
||||
return myCustom
|
||||
}
|
||||
}
|
||||
|
||||
export default HelloModuleService
|
||||
```
|
||||
|
||||
This creates a `HelloModuleService` that uses the service factory to generate data-management methods like `list`, `retrieve`, and `delete`.
|
||||
|
||||
<Note>
|
||||
|
||||
Refer to the full list of generated methods in [this chapter](../service-factory/page.mdx#generated-methods).
|
||||
|
||||
</Note>
|
||||
|
||||
In the service, you also implement the `create` and `update` methods that create and update a `MyCustom` record respectively.
|
||||
|
||||
---
|
||||
|
||||
## 3. Configure Module Definition
|
||||
|
||||
Make sure that the module definition at `src/modules/hello/index.ts` uses the container and connection loaders explained in previous chapters:
|
||||
|
||||
```ts title="src/modules/hello/index.ts"
|
||||
import HelloModuleService from "./service"
|
||||
import { ModulesSdkUtils, MikroOrmBaseRepository } from "@medusajs/utils"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
|
||||
const moduleName = "hello"
|
||||
const pathToMigrations = __dirname + "/migrations"
|
||||
|
||||
const migrationScriptOptions = {
|
||||
moduleName,
|
||||
models: {
|
||||
MyCustom,
|
||||
},
|
||||
pathToMigrations,
|
||||
}
|
||||
|
||||
export const runMigrations = ModulesSdkUtils
|
||||
.buildMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
|
||||
export const revertMigration = ModulesSdkUtils
|
||||
.buildRevertMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
|
||||
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
|
||||
moduleModels: {
|
||||
MyCustom,
|
||||
},
|
||||
moduleRepositories: {
|
||||
BaseRepository: MikroOrmBaseRepository,
|
||||
},
|
||||
moduleServices: [HelloModuleService],
|
||||
})
|
||||
|
||||
const connectionLoader = ModulesSdkUtils.mikroOrmConnectionLoaderFactory({
|
||||
moduleName,
|
||||
moduleModels: [MyCustom],
|
||||
migrationsPath: pathToMigrations,
|
||||
})
|
||||
|
||||
export default {
|
||||
service: HelloModuleService,
|
||||
runMigrations,
|
||||
revertMigration,
|
||||
loaders: [containerLoader, connectionLoader],
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
|
||||
If you don't have the `MyCustom` data model, refer to [this chapter](../../../basics/data-models/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
Also, make sure that the module is added in the `modules` object defined in `medusa-config.js`:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
const modules = {
|
||||
helloModuleService: {
|
||||
resolve: "./dist/modules/hello",
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Add List and Create API Routes
|
||||
|
||||
Create the file `src/api/store/custom/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/store/custom/route.ts"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
|
||||
import HelloModuleService from "../../../modules/hello/service"
|
||||
import { CreateMyCustomDTO } from "../../../types/hello/my-custom"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService =
|
||||
req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
res.json({
|
||||
my_customs: await helloModuleService.list(),
|
||||
})
|
||||
}
|
||||
|
||||
type CustomReq = CreateMyCustomDTO
|
||||
|
||||
export async function POST(
|
||||
req: MedusaRequest<CustomReq>,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService =
|
||||
req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
// skipping validation for simplicity
|
||||
const myCustom = await helloModuleService.create(req.body)
|
||||
|
||||
res.json({
|
||||
my_custom: myCustom,
|
||||
})
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This adds two API routes:
|
||||
|
||||
1. A `GET` API route at `/store/custom` that retrieves a list of `MyCustom` records.
|
||||
2. A `POST` API route at `/store/custom` that creates a `MyCustom` record.
|
||||
|
||||
---
|
||||
|
||||
## 5. Add Retrieve, Update, and Delete API Routes
|
||||
|
||||
Create the file `src/api/store/custom/[id]/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/store/custom/[id]/route.ts"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
|
||||
import HelloModuleService from "../../../../modules/hello/service"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService =
|
||||
req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
const myCustom = await helloModuleService.retrieve(req.params.id)
|
||||
|
||||
res.json({
|
||||
my_custom: myCustom,
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService =
|
||||
req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
// skipping validation for simplicity
|
||||
const myCustom = await helloModuleService.update(
|
||||
req.params.id,
|
||||
req.body
|
||||
)
|
||||
|
||||
res.json({
|
||||
my_custom: myCustom,
|
||||
})
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService =
|
||||
req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
await helloModuleService.delete(req.params.id)
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This creates three API routes:
|
||||
|
||||
1. A `GET` API route at `/store/custom/[id]` that retrieves a `MyCustom` record by its ID.
|
||||
2. A `POST` API route at `/store/custom/[id]` that updates a `MyCustom` record.
|
||||
3. A `DELETE` API route at `/store/custom/[id]` that deletes a `MyCustom` record.
|
||||
|
||||
---
|
||||
|
||||
## 6. Test it Out
|
||||
|
||||
Start your Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, test out each of the API routes you created.
|
||||
|
||||
<Tabs defaultValue="list-route" layoutType="vertical">
|
||||
<TabsList>
|
||||
<TabsTrigger value="list-route">
|
||||
|
||||
<Badge variant="green">GET</Badge> /store/custom
|
||||
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="create-route">
|
||||
|
||||
<Badge variant="blue">POST</Badge> /store/custom
|
||||
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="retrieve-route">
|
||||
|
||||
<Badge variant="green">GET</Badge> /store/custom/[id]
|
||||
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="update-route">
|
||||
|
||||
<Badge variant="blue">POST</Badge> /store/custom/[id]
|
||||
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="delete-route">
|
||||
|
||||
<Badge variant="red">DEL</Badge> /store/custom/[id]
|
||||
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContentWrapper>
|
||||
<TabsContent value="list-route">
|
||||
|
||||
```bash apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/custom"
|
||||
curl http://localhost:9000/store/custom
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="create-route">
|
||||
|
||||
```bash apiTesting testApiMethod="POST" testApiUrl="http://localhost:9000/store/custom" testBodyParams={{ "name": "test" }}
|
||||
curl -X POST http://localhost:9000/store/custom \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"name": "test"
|
||||
}'
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="retrieve-route">
|
||||
|
||||
```bash apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/custom/{id}" testPathParams={{ "id": "mc_123" }}
|
||||
curl http://localhost:9000/store/custom/mc_123
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="update-route">
|
||||
|
||||
```bash apiTesting testApiMethod="POST" testApiUrl="http://localhost:9000/store/custom/{id}" testPathParams={{ "id": "mc_123" }} testBodyParams={{ "name": "test" }}
|
||||
curl -X POST http://localhost:9000/store/custom/mc_123 \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"name": "test"
|
||||
}'
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="delete-route">
|
||||
|
||||
```bash apiTesting testApiMethod="DELETE" testApiUrl="http://localhost:9000/store/custom/{id}" testPathParams={{ "id": "mc_123" }}
|
||||
curl -X DELETE http://localhost:9000/store/custom/mc_123
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
</TabsContentWrapper>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,431 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Example: Module Relationship`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
This chapter gives an example of adding a relationship from your custom module to the Product Module.
|
||||
|
||||
<Note>
|
||||
|
||||
The example in this chapter uses the same `hello` module with the `HelloModuleService` from previous chapters.
|
||||
|
||||
</Note>
|
||||
|
||||
## Create CustomProductData Data Model
|
||||
|
||||
Create the file `src/modules/hello/models/custom-product-data.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/models/custom-product-data.ts"
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
@Entity()
|
||||
export class CustomProductData {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
custom_field: string
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
product_id?: string
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "cpd")
|
||||
}
|
||||
|
||||
@OnInit()
|
||||
OnInit() {
|
||||
this.id = generateEntityId(this.id, "cpd")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This creates a new data model `CustomProductData` to the `hello` module that stores custom fields related to the `Product` data model in the Product Module
|
||||
|
||||
### Add Data Model in MikroORM Configurations
|
||||
|
||||
Add your new data model to the MikroORM configurations file at `src/modules/hello/mikro-orm.config.dev.ts`:
|
||||
|
||||
```ts title="src/modules/hello/mikro-orm.config.dev.ts"
|
||||
// other imports...
|
||||
import { CustomProductData } from "./models/custom-product-data"
|
||||
|
||||
module.exports = {
|
||||
entities: [
|
||||
// other data models...
|
||||
CustomProductData,
|
||||
],
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Add Data Model to Module Definition
|
||||
|
||||
In your module definition at `src/modules/hello/index.ts`, add the data model to the `models` object:
|
||||
|
||||
```ts title="src/modules/hello/index.ts"
|
||||
// other imports...
|
||||
import { CustomProductData } from "./models/custom-product-data"
|
||||
|
||||
const models = {
|
||||
// other models...
|
||||
CustomProductData,
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
|
||||
moduleModels: models,
|
||||
// ...
|
||||
})
|
||||
|
||||
const connectionLoader = ModulesSdkUtils.mikroOrmConnectionLoaderFactory({
|
||||
moduleModels: Object.values(models),
|
||||
// ...
|
||||
})
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
This ensures that the model is passed to the container and connection loader along with other models.
|
||||
|
||||
### Add Data Model Alias in Joiner Configurations
|
||||
|
||||
Add an alias for the data model in the joiner configurations defined in `src/modules/hello/joiner-config.ts`:
|
||||
|
||||
```ts title="src/modules/hello/joiner-config.ts"
|
||||
// other imports...
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { CustomProductData } from "./models/custom-product-data"
|
||||
|
||||
const joinerConfig: ModuleJoinerConfig = {
|
||||
// ...,
|
||||
alias: [
|
||||
// other aliases
|
||||
{
|
||||
name: ["custom_product_data"],
|
||||
args: {
|
||||
entity: CustomProductData.name,
|
||||
methodSuffix: "CustomProductDatas",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### Create Types
|
||||
|
||||
Create the file `src/types/hello/custom-product-data.ts` with the following types:
|
||||
|
||||
```ts title="src/types/hello/custom-product-data.ts"
|
||||
export type CustomProductDataDTO = {
|
||||
id: string
|
||||
custom_field: string
|
||||
product_id?: string
|
||||
}
|
||||
|
||||
export type CreateCustomProductDataDTO = {
|
||||
custom_field: string
|
||||
product_id?: string
|
||||
}
|
||||
|
||||
export type UpdateCustomProductDataDTO = {
|
||||
custom_field?: string
|
||||
product_id?: string
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
You’ll use those in the service next.
|
||||
|
||||
### Add Data Model to Main Module Service
|
||||
|
||||
In the main module service defined at `src/modules/hello/service.ts`, make the following changes to generate and add methods for the new `CustomProductData` model:
|
||||
|
||||
export const serviceHighlights = [
|
||||
["10", "customProductDataService", "Inject the generated service for the `CustomProductData` data model."],
|
||||
["16", "CustomProductData", "Specify the expected input/output type of the data model."],
|
||||
["23", "CustomProductData", "Specify the `CustomProductData` data model to generate methods for it."],
|
||||
["49", "", "Set the `customProductDataService_` class field to the injected `customProductDataService` service."],
|
||||
["55", "createCustomProductData", "Add a method that creates a `CustomProductData` record."]
|
||||
]
|
||||
|
||||
```ts title="src/modules/hello/service.ts" highlights={serviceHighlights}
|
||||
// other imports...
|
||||
import { CustomProductData } from "./models/custom-product-data"
|
||||
import {
|
||||
CreateCustomProductDataDTO,
|
||||
CustomProductDataDTO,
|
||||
} from "../../types/hello/custom-product-data"
|
||||
|
||||
type InjectedDependencies = {
|
||||
// other injected dependencies...
|
||||
customProductDataService:
|
||||
ModulesSdkTypes.InternalModuleService<any>
|
||||
}
|
||||
|
||||
type AllModelsDTO = {
|
||||
// other model DTOs...
|
||||
CustomProductData: {
|
||||
dto: CustomProductDataDTO
|
||||
}
|
||||
}
|
||||
|
||||
const generateMethodsFor = [
|
||||
// other data models generating methods for...
|
||||
CustomProductData,
|
||||
]
|
||||
|
||||
// ...
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory<
|
||||
InjectedDependencies,
|
||||
MyCustomDTO,
|
||||
AllModelsDTO
|
||||
>(MyCustom, generateMethodsFor) {
|
||||
// ...
|
||||
protected customProductDataService_:
|
||||
ModulesSdkTypes.InternalModuleService<CustomProductData>
|
||||
|
||||
constructor(
|
||||
{
|
||||
// other injected dependencies...
|
||||
customProductDataService,
|
||||
}: InjectedDependencies,
|
||||
protected readonly moduleDeclaration:
|
||||
InternalModuleDeclaration
|
||||
) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
// ...
|
||||
this.customProductDataService_ = customProductDataService
|
||||
}
|
||||
|
||||
// other methods...
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async createCustomProductData(
|
||||
data: CreateCustomProductDataDTO,
|
||||
@MedusaContext() context: Context = {}
|
||||
): Promise<CustomProductDataDTO> {
|
||||
const customProductData = await this.customProductDataService_.create(
|
||||
data,
|
||||
context
|
||||
)
|
||||
|
||||
return this.baseRepository_.serialize<CustomProductDataDTO>(
|
||||
customProductData
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the main service you make the following changes:
|
||||
|
||||
1. Add `customProductDataService` to the list of injected dependencies.
|
||||
2. Add the expected input/output type of `CustomProductData` into the `AllModelsDTO` type, which is passed as the third-argument to the `abstractModuleServiceFactory` function.
|
||||
3. Add the data model to the `generateMethodsFor` array, which is passed as a second parameter to the `abstractModuleServiceFactory` function.
|
||||
4. Add a `customProductDataService_` field to the `HelloModuleService` and set its value in the constructor to the injected `customProductDataService`.
|
||||
5. Create a new method `createCustomProductData` that creates a `CustomProductData` record.
|
||||
|
||||
### Generate Migration
|
||||
|
||||
Use the following command to generate the migration file:
|
||||
|
||||
```bash
|
||||
npx cross-env MIKRO_ORM_CLI=./src/modules/hello/mikro-orm.config.dev.ts mikro-orm migration:create
|
||||
```
|
||||
|
||||
### Run Migration
|
||||
|
||||
Run the following commands to run the latest migration:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
npx medusa migrations run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Add Relationship
|
||||
|
||||
To add the relationship from the `hello` module to the Product Module, make the following change in `src/modules/hello/joiner-config.ts`:
|
||||
|
||||
```ts title="src/modules/hello/joiner-config.ts"
|
||||
// other imports...
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
|
||||
const joinerConfig: ModuleJoinerConfig = {
|
||||
// ...
|
||||
relationships: [
|
||||
{
|
||||
serviceName: Modules.PRODUCT,
|
||||
primaryKey: "id",
|
||||
foreignKey: "product_id",
|
||||
alias: "product",
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
This adds a new relationship in the joiner configuration of the `hello` module. The relationship references the `product` alias in the Product Module, which is the alias of the `Product` data model in that module.
|
||||
|
||||
The `product_id` field in your module’s data models references the `id` field of the `Product` data model.
|
||||
|
||||
---
|
||||
|
||||
## Implement Create API Route
|
||||
|
||||
In this section, you’ll implement an API route that creates a product (for simplicity) and then creates a `CustomProductData` record that references that product.
|
||||
|
||||
<Note title="Good to know">
|
||||
|
||||
This example creates a product in a store route to simplify testing the relationship. In a realistic use case, only create products under the `/admin` prefix to ensure that the user is an authenticated admin.
|
||||
|
||||
</Note>
|
||||
|
||||
Create the file `src/api/store/custom-product-data/route.ts` with the following content:
|
||||
|
||||
export const apiRouteHighlights = [
|
||||
["37", "remoteQuery", "Resolve the remote query function from the Medusa container."],
|
||||
["44", "", "Create a product using the parameters in the request body."],
|
||||
["48", "", "Create a `CustomProductData` record with a reference to the created product's ID."],
|
||||
["54", "remoteQueryObjectFromString", "Create the query to pass to the remote query function."],
|
||||
["59", '"product.title"', "Retrieve the product title among the `CustomProductData` record's data."],
|
||||
["60", '"product.handle"', "Retrieve the product handle among the `CustomProductData` record's data."],
|
||||
["63", "", "Filter fetched records by the ID of the created record."],
|
||||
["67", "", "Run the query with the remote query function."],
|
||||
["70", "", "Return the created `CustomProductData` record in the response."]
|
||||
]
|
||||
|
||||
```ts title="src/api/store/custom-product-data/route.ts" highlights={apiRouteHighlights}
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
|
||||
import {
|
||||
CreateCustomProductDataDTO,
|
||||
} from "../../../types/hello/custom-product-data"
|
||||
import HelloModuleService from "../../../modules/hello/service"
|
||||
import type {
|
||||
IProductModuleService,
|
||||
CreateProductDTO,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
remoteQueryObjectFromString,
|
||||
ContainerRegistrationKeys,
|
||||
} from "@medusajs/utils"
|
||||
import type {
|
||||
RemoteQueryFunction,
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
type CreateCustomProductDataReq =
|
||||
CreateCustomProductDataDTO & {
|
||||
product_data: CreateProductDTO
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
req: MedusaRequest<CreateCustomProductDataReq>,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService =
|
||||
req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
const productModuleService: IProductModuleService =
|
||||
req.scope.resolve(
|
||||
"productModuleService"
|
||||
)
|
||||
|
||||
const remoteQuery: RemoteQueryFunction = req.scope.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_QUERY
|
||||
)
|
||||
|
||||
// skipping validation for simplicity
|
||||
const { product_data: productData, ...rest } = req.body
|
||||
|
||||
const product = await productModuleService.create(
|
||||
productData as CreateProductDTO
|
||||
)
|
||||
|
||||
const customProductData = await helloModuleService
|
||||
.createCustomProductData({
|
||||
...rest,
|
||||
product_id: product.id,
|
||||
})
|
||||
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "custom_product_data",
|
||||
fields: [
|
||||
"id",
|
||||
"custom_field",
|
||||
"product.title",
|
||||
"product.handle",
|
||||
],
|
||||
variables: {
|
||||
id: customProductData.id,
|
||||
},
|
||||
})
|
||||
|
||||
const results = await remoteQuery(query)
|
||||
|
||||
res.json({
|
||||
custom_product_data: results[0],
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This API route accepts the necessary request body parameters to create a `CustomProductData` record. It also accepts a `product_data` request body parameter to create the product to be referenced by the `CustomProductData` record.
|
||||
|
||||
In the API route handler, you create the product and then create the `CustomProductData` record.
|
||||
|
||||
Then, you use the remote query function, resolved from the Medusa container, to fetch the created `CustomProductData` record (filtering by its ID) with the product it references. You can add more product-related fields to the `fields` array passed to `remoteQueryObjectFromString`.
|
||||
|
||||
Finally, you return the data returned by the `remoteQuery` function.
|
||||
|
||||
### Test Create API Route
|
||||
|
||||
To test the API route, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, send a `POST` request to the `/store/custom-product-data` route:
|
||||
|
||||
```bash
|
||||
curl --location 'http://localhost:9000/store/custom-product-data' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"product_data": {
|
||||
"title": "Pants"
|
||||
},
|
||||
"custom_field": "nice shirt"
|
||||
}'
|
||||
```
|
||||
|
||||
You’ll receive the following response:
|
||||
|
||||
```json
|
||||
{
|
||||
"custom_product_data": {
|
||||
"id": "cpd_01HWWDD260T0CS64WAF8CVHQ9Y",
|
||||
"custom_field": "nice shirt",
|
||||
"product_id": "prod_01HWWDD25VPQNNZJ57PFTX2JHE",
|
||||
"product": {
|
||||
"title": "Shoes",
|
||||
"handle": "shoes",
|
||||
"id": "prod_01HWWDD25VPQNNZJ57PFTX2JHE"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,146 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Link Modules`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn what a link module is and how to use the remote link in your customizations.
|
||||
|
||||
## What is a Link Module?
|
||||
|
||||
A link module is a module whose only purpose is to define a relationship between two modules’ data models. The relationship is represented as a pivot or link table in the database, pointing at the primary keys of each data model.
|
||||
|
||||
For example, Medusa defines a link module between the Product and Pricing modules. The link module builds a relationship between the `ProductVariant` data model and the `PriceSet` data model.
|
||||
|
||||

|
||||
|
||||
Link modules provide more flexibility in managing relationships between modules while maintaining module isolation.
|
||||
|
||||
Link modules are currently only available for Medusa’s commerce modules. The Medusa application only creates the link tables when both modules are available.
|
||||
|
||||
---
|
||||
|
||||
## What is the Remote Link?
|
||||
|
||||
The remote link is a class with utility methods to manage links between modules. It’s registered in the Medusa container under the `remoteLink` registration name.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
ModuleRegistrationName,
|
||||
RemoteLink,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
export async function POST(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const remoteLink: RemoteLink = req.scope.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_LINK
|
||||
)
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
You can use its methods to manage links, such as create or delete links.
|
||||
|
||||
### Create Link
|
||||
|
||||
To create a link between records of two data models, use the `create` method of the remote link.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { Modules } from "@medusajs/utils"
|
||||
|
||||
// ...
|
||||
|
||||
await remoteLink.create({
|
||||
[Modules.PRODUCT]: {
|
||||
variant_id: product.variants[0].id,
|
||||
},
|
||||
[Modules.PRICING]: {
|
||||
price_set_id: price.id,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules.
|
||||
|
||||
The value of each module’s property is an object. It defines the values of the linked fields.
|
||||
|
||||
So, in the example above, you specify for the Product Module the value of the `variant_id`, and for the Pricing Module the value of `price_set_id`. These are the fields linked between the models of the two modules.
|
||||
|
||||
### Dismiss Link
|
||||
|
||||
To remove a link between records of two data models, use the `dismiss` method of the remote link. This doesn’t remove the records, only the relation between them.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { Modules } from "@medusajs/utils"
|
||||
|
||||
// ...
|
||||
|
||||
await remoteLink.dismiss({
|
||||
[Modules.PRODUCT]: {
|
||||
variant_id: product.variants[0].id,
|
||||
},
|
||||
[Modules.PRICING]: {
|
||||
price_set_id: price.id,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The `dismiss` method accepts the same parameter type as the create method.
|
||||
|
||||
### Cascade Delete Linked Records
|
||||
|
||||
If a record, such as a variant, is deleted, use the `delete` method of the remote link to delete all associated links with cascade delete enabled.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { Modules } from "@medusajs/utils"
|
||||
|
||||
// ...
|
||||
|
||||
await productModuleService.deleteVariants([variant.id])
|
||||
|
||||
await remoteLink.delete({
|
||||
[Modules.PRODUCT]: {
|
||||
variant_id: variant.id,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
This deletes all records linked to the deleted variant with cascade delete enabled in their relationship.
|
||||
|
||||
### Restore Linked Records
|
||||
|
||||
If a record, such as a variant, that was previously soft-deleted is now restored, use the `restore` method of the remote link to restore all associated links that were cascade deleted.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { Modules } from "@medusajs/utils"
|
||||
|
||||
// ...
|
||||
|
||||
await productModuleService.restoreVariants([variant.id])
|
||||
|
||||
await remoteLink.restore({
|
||||
[Modules.PRODUCT]: {
|
||||
variant_id: variant.id,
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,239 @@
|
||||
import { TypeList, CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Module Relationships`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this document, you’ll learn about creating relationships in queryable modules.
|
||||
|
||||
## What is a Relationship in a Queryable Module?
|
||||
|
||||
A queryable module can have a relationship to another queryable module in the form of a reference.
|
||||
|
||||
The Medusa application resolves these relationships while maintaining isolation between the modules and allowing you to retrieve data across them using the remote query.
|
||||
|
||||
<Note title="Use module relationships when" type="success">
|
||||
|
||||
- You want to build relationships between the data models of modules.
|
||||
- You want to assoaciate more fields with the data model of another module.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Don't use module relationships if" type="error">
|
||||
|
||||
- You’re building relationships between data models in the same module. Use foreign keys instead.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Module Relationship?
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
The next chapter provides a detailed example of implementing a relationship between two modules. This section gives the general steps to creating the relationship.
|
||||
|
||||
</Note>
|
||||
|
||||
Consider you’re creating a data model that adds custom fields associated with a product:
|
||||
|
||||
```ts title="src/modules/hello/models/another-custom.ts" highlights={[["13"]]}
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
@Entity()
|
||||
export class CustomProductData {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
custom_field: string
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
product_id?: string
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "cpd")
|
||||
}
|
||||
|
||||
@OnInit()
|
||||
OnInit() {
|
||||
this.id = generateEntityId(this.id, "cpd")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `CustomProductData` data model has a `product_id` field to reference the product it adds custom fields for.
|
||||
|
||||
To create a relationship to the `product` data model in the Product Module, add the following to the joiner configuration of your module:
|
||||
|
||||
export const relationshipsHighlight = [
|
||||
["10", "serviceName", "The name of the module that this relationship is referencing."],
|
||||
["11", "alias", "The alias of the data model you’re referencing in the other module."],
|
||||
["12", "primaryKey", "The name of the field you’re referencing in the other module’s data model."],
|
||||
["13", "foreignKey", "The name of the field in your data models referencing the other module’s model."],
|
||||
]
|
||||
|
||||
```ts title="src/modules/hello/joiner-config.ts" highlights={relationshipsHighlight}
|
||||
// other imports...
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { CustomProductData } from "./models/custom-product-data"
|
||||
|
||||
const joinerConfig: ModuleJoinerConfig = {
|
||||
// ...
|
||||
relationships: [
|
||||
{
|
||||
serviceName: Modules.PRODUCT,
|
||||
alias: "product",
|
||||
primaryKey: "id",
|
||||
foreignKey: "product_id",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default joinerConfig
|
||||
```
|
||||
|
||||
You pass a new property `relationships` to the `joinerConfig`. This property’s value is an array of relationship definitions from your module to other modules.
|
||||
|
||||
Each relationship definition object accepts the following properties:
|
||||
|
||||
<TypeList types={[
|
||||
{
|
||||
name: "serviceName",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The name of the module (as added in `medusa-config.js`) that this relationship is referencing. If you’re referencing a Medusa commerce module, use the `Modules` enum imported from `@medusajs/modules-sdk`."
|
||||
},
|
||||
{
|
||||
name: "alias",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The alias of the data model you’re referencing in the other module. The alias is defined in the joiner configuration of that module, similar to your module."
|
||||
},
|
||||
{
|
||||
name: "primaryKey",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The name of the field you’re referencing in the other module’s data model."
|
||||
},
|
||||
{
|
||||
name: "foreignKey",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The name of the field in your data models referencing the other module’s model."
|
||||
}
|
||||
]} />
|
||||
|
||||
So, the above example creates a relationship to the `Product` data model of the Product Module, which has the alias `product`. `product_id` fields in your module’s data models are considered references to the `id` field of the `Product` data model in the Product Module.
|
||||
|
||||
---
|
||||
|
||||
## Query Data Across Relationships
|
||||
|
||||
The remote query allows you to query data across modules using their relationship without the actual dependency between the modules.
|
||||
|
||||
To do that, specify within the `fields` retrieved in the query the referenced data model’s fields. For example:
|
||||
|
||||
```ts highlights={[["3", '"product.title"', "Retrieve the referenced product's title."]]}
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "another_custom",
|
||||
fields: ["id", "custom_field", "product.title"],
|
||||
})
|
||||
|
||||
const result = await remoteQuery(query)
|
||||
```
|
||||
|
||||
Use `alias`'s value in the relationship definition to reference the other module’s data model. To specify its fields, use dot notation.
|
||||
|
||||
So, in the example above, the referenced product’s title is retrieved along with the ID under the `product` property of each record object in the result.
|
||||
|
||||
---
|
||||
|
||||
## Reference Inner Data Models
|
||||
|
||||
If the data model you’re referencing isn’t the main data model of the main module service, pass to the relationship definition the `args` property:
|
||||
|
||||
```ts title="src/modules/hello/joiner-config.ts" highlights={[["10", "methodSuffix", "The suffix of the referenced data model's methods."]]}
|
||||
const joinerConfig: ModuleJoinerConfig = {
|
||||
// ...
|
||||
relationships: [
|
||||
{
|
||||
serviceName: Modules.PRODUCT,
|
||||
primaryKey: "id",
|
||||
foreignKey: "variant_id",
|
||||
alias: "variant",
|
||||
args: {
|
||||
methodSuffix: "Variants",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
The `args` property’s value is an object accepting a `methodSuffix` property. The `methodSuffix` property’s value is the plural name of the data model.
|
||||
|
||||
---
|
||||
|
||||
## Inverse Relationship
|
||||
|
||||
If you’re creating a relationship between two custom modules, you can define the relationship on the referenced side using the `extends` property of the joiner configuration.
|
||||
|
||||
For example:
|
||||
|
||||
<CodeTabs groupId="inverse-relation">
|
||||
<CodeTab label="Hello Module" value="hello-module">
|
||||
|
||||
```ts title="src/modules/hello/joiner-config.ts"
|
||||
// Hello module
|
||||
const joinerConfig: ModuleJoinerConfig = {
|
||||
// ...
|
||||
relationships: [
|
||||
{
|
||||
serviceName: "anotherHelloModuleService",
|
||||
alias: "another_custom",
|
||||
primaryKey: "id",
|
||||
foreignKey: "another_custom_id",
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="Another Hello Module" value="another-hello-module">
|
||||
|
||||
```ts title="src/modules/another-hello/joiner-config.ts" highlights={[["6", "serviceName", "The name of the module this relationship was originally created in."], ["7", "relationship", "The relationship object as defined in the referencing module."]]}
|
||||
// Another hello module
|
||||
const joinerConfig: ModuleJoinerConfig = {
|
||||
// ...
|
||||
extends: [
|
||||
{
|
||||
serviceName: "helloModuleService",
|
||||
relationship: {
|
||||
serviceName: "anotherHelloModuleService",
|
||||
alias: "another_custom",
|
||||
primaryKey: "id",
|
||||
foreignKey: "another_custom_id",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
The `extends` property’s value is an array of objects having the following properties:
|
||||
|
||||
1. `serviceName`: The name of the module this relationship was originally created in.
|
||||
2. `relationship`: The relationship object as defined in the referencing module.
|
||||
@@ -0,0 +1,147 @@
|
||||
import { CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Module Options`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about passing options to your Module from the Medusa application’s configurations and using them in the Module’s resources.
|
||||
|
||||
## 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 when they’re added to the Medusa application’s configuration.
|
||||
|
||||
---
|
||||
|
||||
## How to Pass Options to a Module?
|
||||
|
||||
To pass options to a module, add an `options` property to the module’s configuration in `medusa-config.js`.
|
||||
|
||||
For example:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
const modules = {
|
||||
helloModuleService: {
|
||||
resolve: "./dist/modules/hello",
|
||||
options: {
|
||||
capitalize: true,
|
||||
},
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The `options` property’s value is an object. You can pass any properties you want.
|
||||
|
||||
---
|
||||
|
||||
## Access Module Options in Main Service
|
||||
|
||||
The module’s main service receives the module’s declaration as a second parameter. The module declaration is an object having an `options` property. Use it to access the options passed from the Module’s configurations.
|
||||
|
||||
For example:
|
||||
|
||||
<CodeTabs groupId="service-type">
|
||||
<CodeTab label="Using Service Factory" value="service-factory">
|
||||
|
||||
```ts highlights={[["24"], ["28"], ["29"], ["30"]]}
|
||||
// other imports...
|
||||
import { InternalModuleDeclaration } from "@medusajs/types"
|
||||
|
||||
// ...
|
||||
|
||||
// recommended to define type in another file
|
||||
type HelloModuleOptions = {
|
||||
capitalize?: boolean
|
||||
}
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory<
|
||||
// ...
|
||||
>(
|
||||
// ...
|
||||
) {
|
||||
// ...
|
||||
protected options_: HelloModuleOptions
|
||||
|
||||
constructor(
|
||||
{
|
||||
// ...
|
||||
}: InjectedDependencies,
|
||||
protected readonly moduleDeclaration: InternalModuleDeclaration
|
||||
) {
|
||||
//...
|
||||
|
||||
this.options_ = moduleDeclaration.options || {
|
||||
capitalize: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="Without Service Factory" value="no-service-factory">
|
||||
|
||||
```ts highlights={[["9"], ["13"], ["14"], ["16"], ["17"], ["18"]]}
|
||||
import { InternalModuleDeclaration } from "@medusajs/types"
|
||||
|
||||
// recommended to define type in another file
|
||||
type HelloModuleOptions = {
|
||||
capitalize?: boolean
|
||||
}
|
||||
|
||||
export default class HelloModuleService {
|
||||
protected options_: HelloModuleOptions
|
||||
|
||||
constructor(
|
||||
{},
|
||||
protected readonly moduleDeclaration:
|
||||
InternalModuleDeclaration
|
||||
) {
|
||||
this.options_ = moduleDeclaration.options || {
|
||||
capitalize: false,
|
||||
}
|
||||
}
|
||||
|
||||
getMessage() {
|
||||
return "Hello, world!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
---
|
||||
|
||||
## Access Module Options in Loader
|
||||
|
||||
The object that a module’s loaders receive as a parameter has an `options` property. Use it to access the options passed from the Module’s configurations.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/modules/hello/loaders/hello-world.ts" highlights={[["11"], ["16"]]}
|
||||
import {
|
||||
LoaderOptions,
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
// recommended to define type in another file
|
||||
type HelloModuleOptions = {
|
||||
capitalize?: boolean
|
||||
}
|
||||
|
||||
export default function helloWorldLoader({
|
||||
options,
|
||||
}: LoaderOptions<HelloModuleOptions>) {
|
||||
|
||||
console.log(
|
||||
"[HELLO MODULE] Just started the Medusa application!",
|
||||
options
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,178 @@
|
||||
import { TypeList } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Queryable Modules`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn what a queryable module is and how to configure a module to be queryable.
|
||||
|
||||
## What is a Queryable Module?
|
||||
|
||||
A queryable module is one whose data can be queried more flexibly and referenced in other modules.
|
||||
|
||||
By making a module queryable, you can fetch its data using object and GraphQL syntax without using the main service’s methods.
|
||||
|
||||
<Note title="Use queryable modules when" type="success">
|
||||
|
||||
- You want more flexibility when fetching data.
|
||||
- You’re creating relations between modules and querying data across modules.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## How to Make a Module Queryable
|
||||
|
||||
<Note title="Steps Summary">
|
||||
|
||||
1. Configure module to be queryable.
|
||||
2. Add `__joinerConfig` method to main service.
|
||||
|
||||
</Note>
|
||||
|
||||
### 1. Adjust Module Configuration
|
||||
|
||||
To make a module queryable, adjust its configuration object passed to the `modules` object in `medusa-config.js`. The configuration object accepts a `definition` property, whose value is an object of advanced module configurations.
|
||||
|
||||
For example:
|
||||
|
||||
export const configHighlights = [
|
||||
["5", "key", "The module's key in the `modules` object."],
|
||||
["6", "registrationName", "The name that the main service is registered under in the Medusa container. It’s recommended to be the same as `key`'s value."],
|
||||
["7", "isQueryable", "Whether the module is queryable."]
|
||||
]
|
||||
|
||||
```js title="medusa-config.js" highlights={configHighlights}
|
||||
const modules = {
|
||||
helloModuleService: {
|
||||
resolve: "./dist/modules/hello",
|
||||
definition: {
|
||||
key: "helloModuleService",
|
||||
registrationName: "helloModuleService",
|
||||
isQueryable: true,
|
||||
},
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The `definition` property’s value accepts the following properties, among others:
|
||||
|
||||
<TypeList types={[
|
||||
{
|
||||
name: "key",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The module's key in the `modules` object."
|
||||
},
|
||||
{
|
||||
name: "registrationName",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The name that the main service is registered under in the Medusa container. It’s recommended to be the same as `key`'s value.",
|
||||
},
|
||||
{
|
||||
name: "isQueryable",
|
||||
type: "`boolean`",
|
||||
description: "Whether the module is queryable."
|
||||
}
|
||||
]} sectionTitle="Adjust Module Configuration" />
|
||||
|
||||
### 2. Define `__joinerConfig` Method
|
||||
|
||||
Queryable modules must define a public `__joinerConfig` method in their main service. This method returns an object that defines how a module’s data can be accessed and referenced.
|
||||
|
||||
Create the file `src/modules/hello/joiner-config.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/joiner-config.ts"
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
|
||||
const joinerConfig: ModuleJoinerConfig = {
|
||||
serviceName: "helloModuleService",
|
||||
primaryKeys: ["id"],
|
||||
alias: [
|
||||
{
|
||||
name: ["my_custom"],
|
||||
args: {
|
||||
entity: MyCustom.name,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default joinerConfig
|
||||
```
|
||||
|
||||
The `joinerConfig` object of type `ModuleJoinerConfig` imported from `@medusajs/types` accepts the following properties:
|
||||
|
||||
<TypeList types={[
|
||||
{
|
||||
name: "serviceName",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The name of the main module service as registered in the Medusa container."
|
||||
},
|
||||
{
|
||||
name: "primaryKeys",
|
||||
type: "`string[]`",
|
||||
optional: false,
|
||||
description: "The primary key field names used in the data models of the module.",
|
||||
},
|
||||
{
|
||||
name: "alias",
|
||||
type: "`object[]`",
|
||||
description: "The alias definitions for each data model in the module.",
|
||||
children: [
|
||||
{
|
||||
name: "name",
|
||||
type: "`string[]`",
|
||||
description: "The alias names used later when fetching or referencing the data in the model."
|
||||
},
|
||||
{
|
||||
name: "args",
|
||||
type: "`object`",
|
||||
description: "The alias's arguments.",
|
||||
children: [
|
||||
{
|
||||
name: "entity",
|
||||
type: "string",
|
||||
description: "The name of the data model this alias is defined for."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]} sectionTitle="Define __joinerConfig Method" />
|
||||
|
||||
Then, import that file in `src/modules/hello/service.ts` and add a new `__joinerConfig` method:
|
||||
|
||||
```ts title="src/modules/hello/service.ts" highlights={[["3"], ["14"], ["15"], ["16"]]}
|
||||
// other imports...
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import joinerConfig from "./joiner-config"
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory<
|
||||
// ...
|
||||
>(
|
||||
// ...
|
||||
) {
|
||||
|
||||
// ...
|
||||
|
||||
__joinerConfig(): ModuleJoinerConfig {
|
||||
return joinerConfig
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Querying a Queryable Module
|
||||
|
||||
The next chapter explains how to query the data of a Queryable module.
|
||||
@@ -0,0 +1,392 @@
|
||||
import { TypeList, Tabs, TabsList, TabsTrigger, TabsContent, TabsContentWrapper } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Remote Query`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about the remote query and how to use it to fetch data from modules.
|
||||
|
||||
## What is the Remote Query?
|
||||
|
||||
The remote query fetches data across queryable modules. It’s a function registered in the Medusa container under the `remoteQuery` key.
|
||||
|
||||
In your resources, such as API routes or workflows, you can resolve the remote query to fetch data across custom modules and Medusa’s commerce modules.
|
||||
|
||||
---
|
||||
|
||||
## Example: Query Hello Module
|
||||
|
||||
For example, create the route `src/api/store/query/route.ts` with the following content:
|
||||
|
||||
export const exampleHighlights = [
|
||||
["18", "", "Resolve the remote query from the Medusa container."],
|
||||
["21", "remoteQueryObjectFromString", "Utility function to build the query."],
|
||||
["22", "entryPoint", "The alias name of the model you’re querying."],
|
||||
["23", "fields", "An array of the data model’s field names to retrieve in the result."],
|
||||
["27", "remoteQuery", "Run the query using the remote query."]
|
||||
]
|
||||
|
||||
```ts title="src/api/store/query/route.ts" highlights={exampleHighlights}
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
remoteQueryObjectFromString,
|
||||
ContainerRegistrationKeys,
|
||||
} from "@medusajs/utils"
|
||||
import type {
|
||||
RemoteQueryFunction,
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const remoteQuery: RemoteQueryFunction = req.scope.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_QUERY
|
||||
)
|
||||
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "my_custom",
|
||||
fields: ["name", "id"],
|
||||
})
|
||||
|
||||
res.json({
|
||||
my_customs: await remoteQuery(query),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, you resolve `remoteQuery` from the Medusa container.
|
||||
|
||||
Then, you create a query using the `remoteQueryObjectFromString` utility function imported from `@medusajs/utils`. This function accepts as a parameter an object with the following required properties:
|
||||
|
||||
- `entryPoint`: The alias name of the model you’re querying. In the previous chapter, you added to the joiner configurations of the module the alias `my_custom` for the `MyCustom` data model.
|
||||
- `fields`: An array of the data model’s field names to retrieve in the result.
|
||||
|
||||
You then pass the query to the `remoteQuery` function to retrieve the results.
|
||||
|
||||
### Test API Route
|
||||
|
||||
To test out the API route, run the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, send a `GET` request to the `/store/query` API route:
|
||||
|
||||
```bash apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/query"
|
||||
curl http://localhost:9000/store/query
|
||||
```
|
||||
|
||||
You’ll receive an array of `MyCustom` records under the `my_customs` key in the JSON response.
|
||||
|
||||
---
|
||||
|
||||
## Apply Filters
|
||||
|
||||
The `remoteQueryObjectFromString` function accepts a `variable` property. You can use this property to filter retrieved records.
|
||||
|
||||
For example:
|
||||
|
||||
```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "my_custom",
|
||||
fields: ["name", "id"],
|
||||
variables: {
|
||||
filters: {
|
||||
id: "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const result = await remoteQuery(query)
|
||||
```
|
||||
|
||||
The `variable` property’s value is an object having the property `filters`, whose value is also an object. The object’s properties are the field names, and the values are the filters to apply.
|
||||
|
||||
You can also filter by multiple values using array values. For example:
|
||||
|
||||
```ts highlights={[["6"], ["7"], ["8"], ["9"]]}
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "my_custom",
|
||||
fields: ["name", "id"],
|
||||
variables: {
|
||||
filters: {
|
||||
id: [
|
||||
"mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
|
||||
"mc_01HWSVWK3KYHKQEE6QGS2JC3FX",
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const result = await remoteQuery(query)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sort Records
|
||||
|
||||
To sort returned records, pass an `order` property to the `variables` property's value. The `order` property is an object whose keys are field names, and values are either:
|
||||
|
||||
- `ASC` to sort records by that field in ascending order.
|
||||
- `DESC` to sort records by that field in descending order.
|
||||
|
||||
For example:
|
||||
|
||||
```ts highlights={[["4"], ["5"], ["6"]]}
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "my_custom",
|
||||
variables: {
|
||||
order: {
|
||||
name: "DESC",
|
||||
},
|
||||
},
|
||||
fields: ["name", "id"],
|
||||
})
|
||||
|
||||
const result = await remoteQuery(query)
|
||||
```
|
||||
|
||||
This retrieves the `MyCustom` records sorted by their name in descending order.
|
||||
|
||||
---
|
||||
|
||||
## Apply Pagination
|
||||
|
||||
To paginate the returned records, pass the following properties to the `variables` property's value:
|
||||
|
||||
- `skip`: (required to apply pagination) The number of records to skip before fetching the results.
|
||||
- `take`: The number of records to fetch.
|
||||
|
||||
For example:
|
||||
|
||||
```ts highlights={[["4", "", "The number of records to skip before fetching the results."], ["5", "", "The number of records to fetch."]]}
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "my_custom",
|
||||
variables: {
|
||||
skip: 0,
|
||||
take: 10,
|
||||
},
|
||||
fields: ["name", "id"],
|
||||
})
|
||||
|
||||
const {
|
||||
rows,
|
||||
metadata: { count, take, skip },
|
||||
} = await remoteQuery(query)
|
||||
```
|
||||
|
||||
This skips no records and returns the first `10` records.
|
||||
|
||||
When the pagination fields are provided, the `remoteQuery` returns an object having two properties:
|
||||
|
||||
<TypeList types={[
|
||||
{
|
||||
name: "rows",
|
||||
type: "`array`",
|
||||
description: "The returned records."
|
||||
},
|
||||
{
|
||||
name: "metadata",
|
||||
type: "`object`",
|
||||
description: "The pagination details",
|
||||
children: [
|
||||
{
|
||||
name: "skip",
|
||||
type: "`number`",
|
||||
description: "The number of records skipped."
|
||||
},
|
||||
{
|
||||
name: "take",
|
||||
type: "`number`",
|
||||
description: "The number of records requested to fetch."
|
||||
},
|
||||
{
|
||||
name: "count",
|
||||
type: "`number`",
|
||||
description: "The total number of records."
|
||||
}
|
||||
]
|
||||
}
|
||||
]} sectionTitle="Apply Pagination" />
|
||||
|
||||
---
|
||||
|
||||
## Using GraphQL
|
||||
|
||||
The remote query function alternatively accepts a string with GraphQL syntax as the query.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/store/query/route.ts" apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/query"
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
import type {
|
||||
RemoteQueryFunction,
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const remoteQuery: RemoteQueryFunction = req.scope.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_QUERY
|
||||
)
|
||||
|
||||
const query = `
|
||||
query {
|
||||
my_custom {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
res.json({
|
||||
my_customs: await remoteQuery(query),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This runs a GraphQL query to retrieve `MyCustom` records.
|
||||
|
||||
### Example Usages
|
||||
|
||||
<Tabs defaultValue="filters" layoutType="vertical" className="mt-2">
|
||||
<TabsList>
|
||||
<TabsTrigger value="filters">Apply Filters</TabsTrigger>
|
||||
<TabsTrigger value="sort">Sort Records</TabsTrigger>
|
||||
<TabsTrigger value="pagination">Apply Pagination</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContentWrapper>
|
||||
<TabsContent value="filters" className="[&_h3]:!mt-0">
|
||||
|
||||
### Apply Filters with GraphQL
|
||||
|
||||
The `remoteQuery` function accepts as a second parameter an object of variables to reference in the GraphQL query.
|
||||
|
||||
For example, to filter the items by their ID:
|
||||
|
||||
```ts highlights={[["2"], ["3"], ["12"], ["13"], ["14"]]}
|
||||
const query = `
|
||||
query($id: ID) {
|
||||
my_custom(id: $id) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const result = await remoteQuery(
|
||||
query,
|
||||
{
|
||||
id: "mc_01HWSVWK3KYHKQEE6QGS2JC3FX"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The variable’s value can also be an array to match multiple items.
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="sort" className="[&_h3]:!mt-0">
|
||||
|
||||
### Sort Records with GraphQL
|
||||
|
||||
To sort the records by a field, pass an `order` argument whose value is an object. The object’s key is the field’s name, and its value is either:
|
||||
|
||||
- `ASC` to sort items by that field in ascending order.
|
||||
- `DESC` to sort items by that field in descending order.
|
||||
|
||||
For example:
|
||||
|
||||
```ts highlights={[["3"]]}
|
||||
const query = `
|
||||
query {
|
||||
my_custom(order: {name: DESC}) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const result = await remoteQuery(query)
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="pagination" className="[&_h3]:!mt-0">
|
||||
|
||||
### Pagination with GraphQL
|
||||
|
||||
To paginate the records retrieved, pass a `skip` and `take` records in your query, and pass their values in the second parameter of the `remoteQuery` function.
|
||||
|
||||
For example:
|
||||
|
||||
```ts highlights={[["2"], ["3"]]}
|
||||
const query = `
|
||||
query($skip: Int, $take: Int) {
|
||||
my_custom(skip: $skip, take: $take) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const {
|
||||
rows,
|
||||
metadata: { count, take, skip }
|
||||
} = await remoteQuery(
|
||||
query,
|
||||
{
|
||||
skip: 0,
|
||||
take: 10
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This skips no records and returns the first `10` records.
|
||||
|
||||
When the pagination fields are provided, the `remoteQuery` returns an object having two properties:
|
||||
|
||||
<TypeList types={[
|
||||
{
|
||||
name: "rows",
|
||||
type: "`array`",
|
||||
description: "The returned records."
|
||||
},
|
||||
{
|
||||
name: "metadata",
|
||||
type: "`object`",
|
||||
description: "The pagination details",
|
||||
children: [
|
||||
{
|
||||
name: "skip",
|
||||
type: "`number`",
|
||||
description: "The number of records skipped."
|
||||
},
|
||||
{
|
||||
name: "take",
|
||||
type: "`number`",
|
||||
description: "The number of records requested to fetch."
|
||||
},
|
||||
{
|
||||
name: "count",
|
||||
type: "`number`",
|
||||
description: "The total number of records."
|
||||
}
|
||||
]
|
||||
}
|
||||
]} sectionTitle="Pagination with GraphQL" />
|
||||
|
||||
</TabsContent>
|
||||
</TabsContentWrapper>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,160 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Service Factory`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this document, you’ll learn about what the service factory is and how to use it to create a service.
|
||||
|
||||
## What is the Service Factory?
|
||||
|
||||
Medusa provides a service factory that your module’s services can extend. The service factory implements data management methods for the data models you specify.
|
||||
|
||||
If your service provides functionalities to manage the records of a data model, it’s encouraged to extend the service factory.
|
||||
|
||||
---
|
||||
|
||||
## How to Extend the Service Factory?
|
||||
|
||||
<Note type="check">
|
||||
|
||||
Your module must be using the following loaders from the previous chapters:
|
||||
|
||||
- [Container Loader](../container/page.mdx).
|
||||
- [Connection Loader](../connection-loader/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
The `ModulesSdkUtils` imported from `@medusajs/utils` has a utility function `abstractModuleServiceFactory` that creates a service instance with basic data management methods.
|
||||
|
||||
So, to create a service that extends the service instance returned by the factory, call `abstractModuleServiceFactory` and extend its returned value:
|
||||
|
||||
```ts title="src/modules/hello/service.ts"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory(
|
||||
MyCustom, []
|
||||
) {
|
||||
// TODO implement custom methods
|
||||
}
|
||||
|
||||
export default HelloModuleService
|
||||
```
|
||||
|
||||
### Function Parameters
|
||||
|
||||
The `abstractModuleServiceFactory` function accepts two parameters:
|
||||
|
||||
1. The first parameter is the main data model this service is creating methods for. It's the `MyCustom` in the example above.
|
||||
2. The second parameter is an array of data models to generate methods for. If you have the `AnotherCustom` data model, this is where you add it.
|
||||
|
||||
### Generated Methods
|
||||
|
||||
The service factory generates the following methods for the main data model:
|
||||
|
||||
- `list`: Returns an array of records based on filters and pagination configurations.
|
||||
- `listAndCount`: Returns a tuple of an array of records and the total count of available records based on the filters and pagination configurations provided.
|
||||
- `retrieve`: Returns a record by its ID.
|
||||
- `delete`: Deletes records by an ID or filter.
|
||||
- `softDelete`: Soft-deletes a record by an ID or filter. This only applies if the data model has a `deleted_at` field.
|
||||
- `restore`: Restores a soft-deleted record by an ID or filter. This only applies if the data model has a `deleted_at` field.
|
||||
|
||||
For other provided data models, the same methods are generated, with the data model’s name appended to the end of the method. For example, `listAnotherCustom` or `retrieveAnotherCustom`.
|
||||
|
||||
### Type Arguments
|
||||
|
||||
For a better development experience and accurate typing of the generated methods, the `abstractModuleServiceFactory` function accepts three type arguments:
|
||||
|
||||
export const typeArgsHighlights = [
|
||||
["24", "InjectedDependencies", "The type of dependencies resolved from the Module's container."],
|
||||
["25", "MyCustomDTO", "The expected input/output type of the main data model's generated methods."],
|
||||
["26", "AllModelsDTO", "The expected input/output type of the generated methods of every data model."],
|
||||
]
|
||||
|
||||
```ts title="src/modules/hello/service.ts" highlights={typeArgsHighlights}
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
|
||||
type MyCustomDTO = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
type InjectedDependencies = {
|
||||
// TODO add dependencies
|
||||
}
|
||||
|
||||
type AllModelsDTO = {
|
||||
MyCustom: {
|
||||
dto: MyCustomDTO
|
||||
}
|
||||
}
|
||||
|
||||
// add other data models in your module here.
|
||||
const generateMethodsFor = []
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory<
|
||||
InjectedDependencies,
|
||||
MyCustomDTO,
|
||||
AllModelsDTO
|
||||
>(MyCustom, generateMethodsFor) {
|
||||
// TODO implement custom methods
|
||||
}
|
||||
|
||||
export default HelloModuleService
|
||||
```
|
||||
|
||||
1. The first one is the type of the injected dependencies received as the first parameter of the service’s constructor. You use this type to specify the resources to resolve from the module’s container.
|
||||
2. The second one is the expected input and output type of the main data model’s methods (for example, the `list` method). This is useful to infer the correct input/output of the generated methods of the main model.
|
||||
3. The third type is the expected input and output type of all data models that the service factory generates methods for (for example, the `listAnotherCustom` method). This is useful to infer the generated methods for each specified data model, with the correct input/output for each of them.
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test out the service, create an API route at `src/api/store/custom/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/store/custom/route.ts"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
|
||||
import HelloModuleService from "../../../modules/hello/service"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService =
|
||||
req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
res.json({
|
||||
my_customs: await helloModuleService.list(),
|
||||
})
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Then, start your Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Finally, send a request `GET` request to `/store/custom`:
|
||||
|
||||
```bash apiTesting testApiUrl="http://localhost:9000/store/custom" testApiMethod="GET"
|
||||
curl http://localhost:9000/store/custom
|
||||
```
|
||||
|
||||
You’ll receive the following response:
|
||||
|
||||
```json
|
||||
{
|
||||
"my_customs": []
|
||||
}
|
||||
```
|
||||
|
||||
In the next chapter, you’ll learn how to add create and update methods to the service generated by the service factory.
|
||||
@@ -0,0 +1,16 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Advanced Development`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the previous chapters, you got a brief introduction to Medusa’s basic concepts. However, to build a custom commerce application, you need a deeper understanding of how you can utilize these concepts for your business use case.
|
||||
|
||||
The next chapters dive deeper into each concept. By the end of these chapters, you’ll be able to:
|
||||
|
||||
- Expose API routes with control over authentication, parsing request bodies, and more.
|
||||
- Create data models with complex fields and relations.
|
||||
- Manage data models in services.
|
||||
- Link data models in your module to data models of other modules.
|
||||
- Emit and handle custom events.
|
||||
- Create advanced workflows with steps across services and third-party systems.
|
||||
@@ -0,0 +1,216 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Data Management Tips`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
This chapter provides some tips when implementing a service that manages a data model.
|
||||
|
||||
## Filter Records
|
||||
|
||||
Medusa provides a `buildQuery` utility method that accepts filters and returns a query object. Then, the query object can be passed to the Repository’s find and list methods to filter the retrieved methods.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/services/my-custom.ts" highlights={[["8"], ["14"]]}
|
||||
// other imports
|
||||
import {
|
||||
Selector,
|
||||
buildQuery,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
class MyCustomService extends TransactionBaseService {
|
||||
async list(
|
||||
selector: Selector<MyCustom>
|
||||
): Promise<MyCustom[]> {
|
||||
const myCustomRepo =
|
||||
this.activeManager_.getRepository(MyCustom)
|
||||
|
||||
const query = buildQuery(selector)
|
||||
|
||||
return await myCustomRepo.find(query)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `buildQuery` method accepts a parameter of type `Selector` provided by Medusa. `Selector` accepts the target data model as a type argument.
|
||||
|
||||
When implementing a `list` method like the above, you can define a selector parameter that can be passed to the method to apply filters on the retrieved records.
|
||||
|
||||
For example, to use this method in another resource:
|
||||
|
||||
```ts
|
||||
const items = await myCustomService.list({
|
||||
name: "John",
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Paginate Records
|
||||
|
||||
The `buildQuery` method accepts an optional second parameter of type `FindConfig`, also provided by Medusa. Like `Selector`, it accepts the target data model as a type argument.
|
||||
|
||||
For example, you can change the implementation of the `list` method to the following:
|
||||
|
||||
```ts title="src/services/my-custom.ts" highlights={[["7", "12"]]}
|
||||
// other imports...
|
||||
import { FindConfig } from "@medusajs/medusa"
|
||||
|
||||
class MyCustomService extends TransactionBaseService {
|
||||
async list(
|
||||
selector: Selector<MyCustom>,
|
||||
config?: FindConfig<MyCustom>
|
||||
): Promise<MyCustom[]> {
|
||||
const myCustomRepo =
|
||||
this.activeManager_.getRepository(MyCustom)
|
||||
|
||||
const query = buildQuery(selector, config)
|
||||
|
||||
return await myCustomRepo.find(query)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You add a new parameter to the `list` method of type `FindConfig<MyCustom>`, then pass that parameter to `buildQuery`.
|
||||
|
||||
You can now pass pagination fields to the method in another resource:
|
||||
|
||||
```ts
|
||||
const items = await myCustomService.list(
|
||||
{},
|
||||
{
|
||||
take: 20,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This returns only the first `20` records in the database.
|
||||
|
||||
Other pagination fields include:
|
||||
|
||||
- `skip`: A number indicating how many items to skip before retrieving the records.
|
||||
- `order`: An object whose keys are names of fields to sort the list by, and value is either `ASC` for ascending sorting or `DESC` for descending sorting.
|
||||
|
||||
---
|
||||
|
||||
## Select Fields and Relations
|
||||
|
||||
The `FindConfig` type accepts two additional properties:
|
||||
|
||||
- `select`: An array of strings, each being the name of a field in the data model that should be retrieved. When not specified, all fields are retrieved.
|
||||
- `relations`: An array of strings, each being the name of a relation in the data model that should be retrieved.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
const items = await myCustomService.list(
|
||||
{},
|
||||
{
|
||||
select: ["name"],
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Management Operations
|
||||
|
||||
The repository provides all necessary operations to manage a data model including creating, updating, and deleting records. For a full list of repository methods, check out [Typeorm’s documentation](https://typeorm.io/repository-api).
|
||||
|
||||
---
|
||||
|
||||
## Example: CRUD Operations
|
||||
|
||||
Below is an example of a service that implements Create, Read, Update, and Delete (CRUD) operations on a data model.
|
||||
|
||||
<Details summaryContent="Example">
|
||||
|
||||
```ts title="src/services/my-custom.ts"
|
||||
import {
|
||||
FindConfig,
|
||||
Selector,
|
||||
TransactionBaseService,
|
||||
buildQuery,
|
||||
} from "@medusajs/medusa"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { MyCustom } from "../models/my-custom"
|
||||
|
||||
class MyCustomService extends TransactionBaseService {
|
||||
async list(
|
||||
selector: Selector<MyCustom>,
|
||||
config?: FindConfig<MyCustom>
|
||||
): Promise<MyCustom[]> {
|
||||
const myCustomRepo =
|
||||
this.activeManager_.getRepository(MyCustom)
|
||||
|
||||
const query = buildQuery(selector, config)
|
||||
|
||||
return await myCustomRepo.find(query)
|
||||
}
|
||||
async retrieve(
|
||||
id: string,
|
||||
config?: FindConfig<MyCustom>
|
||||
): Promise<MyCustom> {
|
||||
const myCustomRepo =
|
||||
this.activeManager_.getRepository(MyCustom)
|
||||
|
||||
const query = buildQuery(
|
||||
{
|
||||
id,
|
||||
},
|
||||
config
|
||||
)
|
||||
const custom = await myCustomRepo.findOne(query)
|
||||
|
||||
if (!custom) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Record was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return custom
|
||||
}
|
||||
|
||||
async create(data: Omit<MyCustom, "id">): Promise<MyCustom> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const myCustomRepo = manager.getRepository(MyCustom)
|
||||
|
||||
const custom = myCustomRepo.create(data)
|
||||
|
||||
return await myCustomRepo.save(custom)
|
||||
})
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
data: Omit<MyCustom, "id">
|
||||
): Promise<MyCustom> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const myCustomRepo = manager.getRepository(MyCustom)
|
||||
const custom = await this.retrieve(id)
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
custom[key] = value
|
||||
}
|
||||
|
||||
return await myCustomRepo.save(custom)
|
||||
})
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const myCustomRepo = manager.getRepository(MyCustom)
|
||||
|
||||
await myCustomRepo.delete({
|
||||
id,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default MyCustomService
|
||||
```
|
||||
|
||||
</Details>
|
||||
@@ -0,0 +1,111 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Data Model Management`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to manage a data model through a service.
|
||||
|
||||
## Entity Manager
|
||||
|
||||
An entity manager allows you to retrieve the repository of a data model, which you can then use to manage records of that model, such as create or update them.
|
||||
|
||||
The `TransactionBaseService` defines an `activeManager_` property, which is an instance of the entity manager. Services can use that property to retrieve a repository.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/services/my-custom.ts" highlights={[["7"]]}
|
||||
import { TransactionBaseService } from "@medusajs/medusa"
|
||||
import { MyCustom } from "../models/my-custom"
|
||||
|
||||
class MyCustomService extends TransactionBaseService {
|
||||
async retrieve(id: string): Promise<MyCustom> {
|
||||
const myCustomRepo =
|
||||
this.activeManager_.getRepository(MyCustom)
|
||||
|
||||
// use repository...
|
||||
}
|
||||
}
|
||||
|
||||
export default MyCustomService
|
||||
```
|
||||
|
||||
You use the entity manager’s `getRepository` method to retrieve the repository of a data model. The method accepts the data model as a parameter.
|
||||
|
||||
---
|
||||
|
||||
## Transaction Entity Managers
|
||||
|
||||
The function parameter of the `atomicPhase_` method accepts an instance of the transaction’s entity manager as a parameter. When retrieving a data model’s repository or performing database operations, you must use the transaction entity manager parameter.
|
||||
|
||||
### Retrieve Repository
|
||||
|
||||
```ts title="src/services/my-custom.ts" highlights={[["5"], ["6"]]}
|
||||
class MyCustomService extends TransactionBaseService {
|
||||
// ...
|
||||
|
||||
async create(data: any): Promise<MyCustom> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const myCustomRepo = manager.getRepository(MyCustom)
|
||||
|
||||
// use repository...
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Services
|
||||
|
||||
If you’re using methods of custom services within the transaction, you must call the `withTransaction` method of the service first and pass the transaction entity manager as a parameter. Then, chain the call with the call to the desired method.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/services/my-custom.ts" highlights={[["8"], ["9"]]}
|
||||
class MyCustomService extends TransactionBaseService {
|
||||
// ...
|
||||
|
||||
async create(data: any): Promise<MyCustom> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
// ...
|
||||
const customData = await this.myOtherCustomService
|
||||
.withTransaction(manager)
|
||||
.retrieve(id)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Commerce Module Services
|
||||
|
||||
All methods of Commerce Module services accept an object as a last parameter that's used to pass shared resources, such as the transaction manager in this case.
|
||||
|
||||
So, when using a Commerce Module service's method in a transaction, pass the transaction manager as part of the last object parameter.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/services/hello-world.ts" highlights={[["17"]]}
|
||||
class HelloWorldService extends TransactionBaseService {
|
||||
// ...
|
||||
async updateProduct(
|
||||
productId: string,
|
||||
data: UpdateProductDTO
|
||||
): Promise<ProductDTO> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
// example of a database operation
|
||||
const product = await this.productModuleService.update(
|
||||
[
|
||||
{
|
||||
...data,
|
||||
id: productId,
|
||||
},
|
||||
],
|
||||
{
|
||||
transactionManager: manager,
|
||||
}
|
||||
)
|
||||
|
||||
return product[0]
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Transactions in Services`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about transactions and how to use them in a service.
|
||||
|
||||
## What is a Transaction?
|
||||
|
||||
A transaction wraps a set of operations to ensure that when an error occurs, all database changes made by the executed operations are rolled back.
|
||||
|
||||
For example, if you have a service with a method that updates data of your custom data model, you can wrap the method’s operations in a transaction. Then, if an error occurs during the method's execution, the data update in the database is rolled back.
|
||||
|
||||
---
|
||||
|
||||
## How to Use Transactions?
|
||||
|
||||
To use transactions, change your service class to extend the `TransactionBaseService` imported from `@medusajs/medusa`:
|
||||
|
||||
```ts title="src/services/hello-world.ts" highlights={[["8"]]}
|
||||
import { TransactionBaseService } from "@medusajs/medusa"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productModuleService: IProductModuleService
|
||||
}
|
||||
|
||||
class HelloWorldService extends TransactionBaseService {
|
||||
protected productModuleService: IProductModuleService
|
||||
|
||||
constructor({ productModuleService }: InjectedDependencies) {
|
||||
super(arguments[0])
|
||||
|
||||
this.productModuleService = productModuleService
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
export default HelloWorldService
|
||||
```
|
||||
|
||||
Then, use the `TransactionBaseService`'s `atomicPhase_` method that allows you to wrap operations within transactions.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/services/hello-world.ts" highlights={[["11"]]}
|
||||
// other imports...
|
||||
import { ProductDTO, UpdateProductDTO } from "@medusajs/types"
|
||||
|
||||
class HelloWorldService extends TransactionBaseService {
|
||||
// ...
|
||||
|
||||
async updateProduct(
|
||||
productId: string,
|
||||
data: UpdateProductDTO
|
||||
): Promise<ProductDTO> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
// TODO perform db operation...
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the `updateProduct` method, you use the `atomicPhase_` method to wrap the method’s implementation.
|
||||
|
||||
The `atomicPhase_` method accepts a function as a parameter. All database operations performed in the function are wrapped in a transaction. So, if an error occurs in the function, the operations’ database changes are rolled back.
|
||||
|
||||
The data returned by the function parameter is then returned by the `atomicPhase_` method.
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Advanced Workflow Example`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll create an advanced workflow.
|
||||
|
||||
Workflows are most useful when performing tasks across services and integrations:
|
||||
|
||||
- You simplify complex flows or processes by splitting them into a series of steps.
|
||||
- You avoid data inconsistency and loss through retry mechanisms and rollbacks.
|
||||
- You execute the workflow from anywhere in your Medusa application.
|
||||
|
||||
The workflow you’ll build is an update-product workflow. You'll use an example `ErpService` that has methods to manage the product in a third-party ERP system, and the `ProductService`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Create Workflow File
|
||||
|
||||
Start by creating the file `src/workflows/update-product-erp/index.ts` that will hold the constructed workflow.
|
||||
|
||||
In the file, add the type of the expected workflow input:
|
||||
|
||||
```ts title="src/workflows/update-product-erp/index.ts"
|
||||
import { UpdateProductDTO } from "@medusajs/types"
|
||||
|
||||
export type UpdateProductAndErpWorkflowInput = UpdateProductDTO
|
||||
|
||||
```
|
||||
|
||||
The expected input is the data to update in the product along with the product’s ID.
|
||||
|
||||
---
|
||||
|
||||
## 2. Create Update Product Step
|
||||
|
||||
The first step in the workflow receives the product’s ID and the data to update, then updates the product.
|
||||
|
||||
Create the file `src/workflows/update-product-erp/steps/update-product.ts` with the following content:
|
||||
|
||||
export const updateProductHighlights = [
|
||||
["13", "resolve", "Resolve the `ProductService` from the Medusa container."],
|
||||
["16", "previousProductData", "Retrieve the `previousProductData` to pass it to the compensation function."],
|
||||
["19", "", "Update the product."],
|
||||
["33", "", "Revert the product’s data using the `previousProductData` passed from the step to the compensation function."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/update-product-erp/steps/update-product.ts" highlights={updateProductHighlights}
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { UpdateProductAndErpWorkflowInput } from ".."
|
||||
|
||||
const updateProduct = createStep(
|
||||
"update-product",
|
||||
async (input: UpdateProductAndErpWorkflowInput, context) => {
|
||||
const productModuleService: IProductModuleService =
|
||||
context.container.resolve(ModuleRegistrationName.PRODUCT)
|
||||
|
||||
const { id } = input
|
||||
const previousProductData =
|
||||
await productModuleService.retrieve(id)
|
||||
|
||||
const product = await productModuleService.update([input])
|
||||
|
||||
return new StepResponse(product, {
|
||||
// pass to compensation function
|
||||
previousProductData,
|
||||
})
|
||||
},
|
||||
// compensation function
|
||||
async ({ previousProductData }, context) => {
|
||||
const productModuleService: IProductModuleService =
|
||||
context.container.resolve(ModuleRegistrationName.PRODUCT)
|
||||
|
||||
const { type, ...previousData } = previousProductData
|
||||
|
||||
await productModuleService.update([
|
||||
{
|
||||
...previousData,
|
||||
},
|
||||
])
|
||||
|
||||
await productModuleService.updateTypes(type)
|
||||
}
|
||||
)
|
||||
|
||||
export default updateProduct
|
||||
```
|
||||
|
||||
In the step:
|
||||
|
||||
- You resolve the `ProductService` from the Medusa container.
|
||||
- You retrieve the `previousProductData` to pass it to the compensation function.
|
||||
- You update and return the product.
|
||||
|
||||
You also pass a compensation function as a second parameter to `createStep`. The compensation function runs if an error occurs during the workflow execution.
|
||||
|
||||
In the compensation function, you revert the product’s data using the `previousProductData` passed from the step to the compensation function.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
The compensation functions of each step ensure data consistency across Medusa and external services and systems when errors occur.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## 3. Create Step 2: Update ERP
|
||||
|
||||
The second step in the workflow receives the same input. It updates the product’s details in the ERP system using the `ErpService`.
|
||||
|
||||
Create the file `src/workflows/update-product-erp/steps/update-erp.ts` with the following content:
|
||||
|
||||
export const updateErpHighlights = [
|
||||
["12", "resolve", "Resolve the `ErpService` from the Medusa container."],
|
||||
["17", "previousErpData", "Retrieve the `previousErpData` to pass it to the compensation function."],
|
||||
["21", "updateProductErpData", "Update the product’s ERP data and return the data from the ERP system."],
|
||||
["34", "updateProductErpData", "Revert the product's data in the ERP system to its previous state using the `previousErpData`."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/update-product-erp/steps/update-erp.ts" highlights={updateErpHighlights}
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { UpdateProductAndErpWorkflowInput } from ".."
|
||||
import ErpService from "../../../services/erp"
|
||||
|
||||
const updateErp = createStep(
|
||||
"update-erp",
|
||||
async (input: UpdateProductAndErpWorkflowInput, context) => {
|
||||
const erpService: ErpService =
|
||||
context.container.resolve("erpService")
|
||||
|
||||
const { id, ...updatedData } = input
|
||||
|
||||
// get previous ERP data
|
||||
const previousErpData =
|
||||
await erpService.retrieveProductErpDetails(id)
|
||||
|
||||
const updatedErpData =
|
||||
await erpService.updateProductErpData(id, updatedData)
|
||||
|
||||
return new StepResponse(updatedErpData, {
|
||||
// pass to compensation function
|
||||
previousErpData,
|
||||
productId: id,
|
||||
})
|
||||
},
|
||||
// compensation function
|
||||
async ({ previousErpData, productId }, context) => {
|
||||
const erpService: ErpService =
|
||||
context.container.resolve("erpService")
|
||||
|
||||
await erpService.updateProductErpData(
|
||||
productId,
|
||||
previousErpData
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default updateErp
|
||||
```
|
||||
|
||||
In the step:
|
||||
|
||||
- You resolve the `ErpService` from the Medusa container.
|
||||
- You retrieve the `previousErpData` to pass it to the compensation function.
|
||||
- You update the product’s ERP data and return the data from the ERP system.
|
||||
|
||||
You also pass a compensation function as a second parameter to `createStep`. In the compensation function, you revert the product's data in the ERP system to its previous state.
|
||||
|
||||
---
|
||||
|
||||
## 4. Create Workflow
|
||||
|
||||
With the steps ready, you'll create the workflow that runs these steps to update the product’s data both in Medusa and the external ERP system.
|
||||
|
||||
Change the content of `src/workflows/update-product-erp/index.ts` to the following:
|
||||
|
||||
```ts title="src/workflows/update-product-erp/index.ts"
|
||||
import { createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import { UpdateProductDTO, ProductDTO } from "@medusajs/types"
|
||||
import updateProduct from "./steps/update-product"
|
||||
import updateErp from "./steps/update-erp"
|
||||
|
||||
export type UpdateProductAndErpWorkflowInput = UpdateProductDTO
|
||||
|
||||
type WorkflowOutput = {
|
||||
product: ProductDTO
|
||||
erpData: Record<string, unknown>
|
||||
}
|
||||
|
||||
const updateProductAndErpWorkflow = createWorkflow<
|
||||
UpdateProductAndErpWorkflowInput,
|
||||
WorkflowOutput
|
||||
>("update-product-and-erp", function (input) {
|
||||
const product = updateProduct(input)
|
||||
const erpData = updateErp(input)
|
||||
|
||||
return {
|
||||
product,
|
||||
erpData,
|
||||
}
|
||||
})
|
||||
|
||||
export default updateProductAndErpWorkflow
|
||||
```
|
||||
|
||||
In the workflow construction function, you first run the `updateProduct` step, then the `updateErp` step. You return as the workflow’s result an object holding the updated product and ERP data.
|
||||
|
||||
---
|
||||
|
||||
## 5. Execute Workflow
|
||||
|
||||
You can now use and execute your workflow in your Medusa application.
|
||||
|
||||
To execute the workflow in an API route, create the file `src/api/store/products/[id]/erp/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/store/products/[id]/erp/route.ts"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
|
||||
import updateProductAndErpWorkflow, {
|
||||
UpdateProductAndErpWorkflowInput,
|
||||
} from "../../../../../workflows/update-product-erp"
|
||||
|
||||
export const POST = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
// skipping validation for simplicity
|
||||
const productData: UpdateProductAndErpWorkflowInput = {
|
||||
id: req.params.id,
|
||||
...req.body,
|
||||
}
|
||||
|
||||
const { result } = await updateProductAndErpWorkflow(
|
||||
req.scope
|
||||
).run({
|
||||
input: productData,
|
||||
})
|
||||
|
||||
res.json(result)
|
||||
}
|
||||
```
|
||||
|
||||
In this `POST` API route, you retrieve the product’s ID from the path parameter and the data to update from the request body. You then execute the workflow by passing it the retrieved data as an input.
|
||||
|
||||
The route returns the result of the workflow, which is an object holding both the update product’s details and the ERP details.
|
||||
@@ -0,0 +1,111 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Compensation Function`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn how to add a compensation function to a step.
|
||||
|
||||
## Compensation Function
|
||||
|
||||
Errors can occur in a workflow. To avoid data inconsistency, you can pass a compensation function as a third parameter to the `createStep` function.
|
||||
|
||||
The compensation function only runs if an error occurs throughout the Workflow. It’s useful to undo or roll back actions you’ve performed in a step.
|
||||
|
||||
For example, change step one to add a compensation function and step two to throw an error:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts" highlights={[["10"], ["11"], ["12"]]}
|
||||
const step1 = createStep(
|
||||
"step-1",
|
||||
async () => {
|
||||
const message = `Hello from step one!`
|
||||
|
||||
console.log(message)
|
||||
|
||||
return new StepResponse(message)
|
||||
},
|
||||
async () => {
|
||||
console.log("Oops! Rolling back my changes...")
|
||||
}
|
||||
)
|
||||
|
||||
const step2 = createStep(
|
||||
"step-2",
|
||||
async ({ name }: WorkflowInput) => {
|
||||
throw new Error("Throwing an error...")
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Test Compensation Function
|
||||
|
||||
1. Use the steps in a workflow. For example:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts"
|
||||
import {
|
||||
// other imports...
|
||||
createWorkflow,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
|
||||
// ...
|
||||
|
||||
type WorkflowOutput = {
|
||||
message: string
|
||||
}
|
||||
|
||||
const myWorkflow = createWorkflow<
|
||||
WorkflowInput,
|
||||
WorkflowOutput
|
||||
>("hello-world", function (input) {
|
||||
const str1 = step1()
|
||||
// to pass input
|
||||
const str2 = step2(input)
|
||||
|
||||
return {
|
||||
message: str1,
|
||||
}
|
||||
})
|
||||
|
||||
export default myWorkflow
|
||||
```
|
||||
|
||||
2. Execute the workflow from a resource, such as an API route:
|
||||
|
||||
```ts title="src/api/store/workflow/route.ts"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import myWorkflow from "../../../workflows/hello-world"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) {
|
||||
const { result } = await myWorkflow(req.scope)
|
||||
.run({
|
||||
input: {
|
||||
name: req.query.name as string,
|
||||
},
|
||||
})
|
||||
|
||||
res.send(result)
|
||||
}
|
||||
```
|
||||
|
||||
3. Run the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. Send a `GET` request to `/store/workflow`:
|
||||
|
||||
```bash apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/workflows"
|
||||
curl http://localhost:9000/store/workflow
|
||||
```
|
||||
|
||||
In the console, you'll see:
|
||||
|
||||
- `Hello from step one!` logged in the terminal, indicating that the first step ran successfully.
|
||||
- `Oops! Rolling back my changes...` logged in the terminal, indicating that the second step failed and the compensation function of the first step ran consequently.
|
||||
@@ -0,0 +1,96 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Constraints on Workflow Constructor Function`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
This chapter lists some constraints to keep in mind when defining Workflow constructor functions.
|
||||
|
||||
## No Arrow Functions
|
||||
|
||||
The function passed to the `createWorkflow` function can’t be an arrow function:
|
||||
|
||||
```ts highlights={[["5", "=>", "Function can't be an arrow function"], ["13", "", "Correct way of defining the function."]]}
|
||||
// Don't
|
||||
const myWorkflow = createWorkflow<
|
||||
WorkflowInput,
|
||||
WorkflowOutput
|
||||
>("hello-world", (input) => {
|
||||
// ...
|
||||
})
|
||||
|
||||
// Do
|
||||
const myWorkflow = createWorkflow<
|
||||
WorkflowInput,
|
||||
WorkflowOutput
|
||||
>("hello-world", function (input) {
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
## No Async Functions
|
||||
|
||||
The function passed to the `createWorkflow` can’t be an async function:
|
||||
|
||||
```ts highlights={[["5", "async", "Function can't be async."], ["13", "", "Correct way of defining the function."]]}
|
||||
// Don't
|
||||
const myWorkflow = createWorkflow<
|
||||
WorkflowInput,
|
||||
WorkflowOutput
|
||||
>("hello-world", async function (input) {
|
||||
// ...
|
||||
})
|
||||
|
||||
// Do
|
||||
const myWorkflow = createWorkflow<
|
||||
WorkflowInput,
|
||||
WorkflowOutput
|
||||
>("hello-world", function (input) {
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
## No Direct Data Manipulation
|
||||
|
||||
Since the constructor function only defines how the workflow works, you can’t directly manipulate data within the function. Instead, use the `transform` function:
|
||||
|
||||
export const highlights = [
|
||||
["10", "", "Don't manipulate data directly."],
|
||||
["22", "transform", "Use the `transform` function to manipulate data."]
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
// Don't
|
||||
const myWorkflow = createWorkflow<
|
||||
WorkflowInput,
|
||||
WorkflowOutput
|
||||
>("hello-world", function (input) {
|
||||
const str1 = step1(input)
|
||||
const str2 = step2(input)
|
||||
|
||||
return {
|
||||
message: `${input.str1}${input.str2}`,
|
||||
}
|
||||
})
|
||||
|
||||
// Do
|
||||
const myWorkflow = createWorkflow<
|
||||
WorkflowInput,
|
||||
WorkflowOutput
|
||||
>("hello-world", function (input) {
|
||||
const str1 = step1(input)
|
||||
const str2 = step2(input)
|
||||
|
||||
const result = transform(
|
||||
{
|
||||
str1,
|
||||
str2,
|
||||
},
|
||||
(input) => ({
|
||||
message: `${input.str1}${input.str2}`,
|
||||
})
|
||||
)
|
||||
|
||||
return result
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,58 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Running Workflow Steps in Parallel`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to run workflow steps in parallel.
|
||||
|
||||
## parallelize Utility Function
|
||||
|
||||
If your workflow has steps that don’t rely on one another’s results, you can run them in parallel. The workflow will wait until all specified steps are finished before continuing with the rest of its implementation.
|
||||
|
||||
The `parallelize` utility function imported from the `@medusajs/workflows-sdk` package allows you to run the steps in parallel.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["23", "[prices, productSalesChannel]", "The result of the steps. `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`."],
|
||||
["23", "parallelize", "Run the steps passed as parameters in parallel."],
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
import {
|
||||
createWorkflow,
|
||||
parallelize,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { ProductDTO } from "@medusajs/types"
|
||||
import {
|
||||
createProductStep,
|
||||
getProductStep,
|
||||
createPricesStep,
|
||||
attachProductToSalesChannelStep,
|
||||
} from "./steps"
|
||||
|
||||
interface WorkflowInput {
|
||||
title: string
|
||||
}
|
||||
|
||||
const myWorkflow = createWorkflow<
|
||||
WorkflowInput,
|
||||
ProductDTO
|
||||
>("my-workflow", (input) => {
|
||||
const product = createProductStep(input)
|
||||
|
||||
const [prices, productSalesChannel] = parallelize(
|
||||
createPricesStep(product),
|
||||
attachProductToSalesChannelStep(product)
|
||||
)
|
||||
|
||||
const id = product.id
|
||||
return getProductStep(product.id)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `parallelize` function accepts the steps to run in parallel as a parameter.
|
||||
|
||||
It returns an array of each step’s result. The results are ordered in the result array by the order they're passed in the function's parameter.
|
||||
@@ -0,0 +1,29 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Architectural Modules`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about architectural modules.
|
||||
|
||||
## What is an Architectural Module?
|
||||
|
||||
An architectural module implements features and mechanisms related to the Medusa application’s architecture and infrastructure.
|
||||
|
||||
Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis.
|
||||
|
||||
Medusa currently supports two types of architectural modules: Cache and event modules.
|
||||
|
||||
<Note>
|
||||
|
||||
Refer to the [Architectural Modules reference](!resources!/architectural-modules) for a list of Medusa’s architectural modules.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## How to Create an Architectural Module?
|
||||
|
||||
You create an architectural module like you’ve been creating custom modules: create a module with a service and export that service in the module’s definition file. The module's service extends an abstract class or interface provided by Medusa with predefined method signatures.
|
||||
|
||||
The next chapters explain the different architectural module types and how to create an architectural module for each.
|
||||
@@ -0,0 +1,165 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} How to Create a Cache Module`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to create a Cache Module.
|
||||
|
||||
## 1. Create Module Directory
|
||||
|
||||
Start by creating a new directory for your module. For example, `src/modules/my-cache`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Create the Cache Service
|
||||
|
||||
Create the file `src/modules/my-cache/service.ts` that holds the implementation of the cache service.
|
||||
|
||||
The Cache Module's main service must implement the `ICacheService` interface imported from `@medusajs/types`:
|
||||
|
||||
```ts title="src/modules/my-cache/service.ts"
|
||||
import { ICacheService } from "@medusajs/types"
|
||||
|
||||
class MyCacheService implements ICacheService {
|
||||
get<T>(key: string): Promise<T> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
set(key: string, data: unknown, ttl?: number): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
invalidate(key: string): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
export default MyCacheService
|
||||
```
|
||||
|
||||
The service implements the required methods based on the desired caching mechanism.
|
||||
|
||||
### Implement get Method
|
||||
|
||||
The `get` method retrieves the value of a cached item based on its key.
|
||||
|
||||
The method accepts a string as a first parameter, which is the key in the cache. It either returns the cached item or `null` if it doesn’t exist.
|
||||
|
||||
For example, to implement this method using Memcached:
|
||||
|
||||
```ts title="src/modules/my-cache/service.ts"
|
||||
class MyCacheService implements ICacheService {
|
||||
// ...
|
||||
async get<T>(cacheKey: string): Promise<T | null> {
|
||||
return new Promise((res, rej) => {
|
||||
this.memcached.get(cacheKey, (err, data) => {
|
||||
if (err) {
|
||||
res(null)
|
||||
} else {
|
||||
if (data) {
|
||||
res(JSON.parse(data))
|
||||
} else {
|
||||
res(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Implement set Method
|
||||
|
||||
The `set` method is used to set an item in the cache. It accepts three parameters:
|
||||
|
||||
1. The first parameter is a string indicating the key of the data being added to the cache. This key can be used later to get or invalidate the cached item.
|
||||
2. The second parameter is the data to be added to the cache. The data can be of any type.
|
||||
3. The third parameter is optional. It’s a number indicating how long (in seconds) the data should be kept in the cache.
|
||||
|
||||
For example, to implement this method using Memcached:
|
||||
|
||||
```ts title="src/modules/my-cache/service.ts"
|
||||
class MyCacheService implements ICacheService {
|
||||
protected TTL = 60
|
||||
// ...
|
||||
async set(
|
||||
key: string,
|
||||
data: Record<string, unknown>,
|
||||
ttl: number = this.TTL // or any value
|
||||
): Promise<void> {
|
||||
return new Promise((res, rej) =>
|
||||
this.memcached.set(
|
||||
key, JSON.stringify(data), ttl, (err) => {
|
||||
if (err) {
|
||||
rej(err)
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Implement invalidate Method
|
||||
|
||||
The `invalidate` method removes an item from the cache using its key.
|
||||
|
||||
By default, items are removed from the cache when their time-to-live (ttl) expires. The `invalidate` method can be used to remove the item beforehand.
|
||||
|
||||
The method accepts a string as a first parameter, which is the key of the item to invalidate and remove from the cache.
|
||||
|
||||
For example, to implement this method using Memcached:
|
||||
|
||||
```ts title="src/modules/my-cache/service.ts"
|
||||
class MyCacheService implements ICacheService {
|
||||
// ...
|
||||
async invalidate(key: string): Promise<void> {
|
||||
return new Promise((res, rej) => {
|
||||
this.memcached.del(key, (err) => {
|
||||
if (err) {
|
||||
rej(err)
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Create Module Definition File
|
||||
|
||||
Create the file `src/modules/my-cache/index.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/my-cache/index.ts"
|
||||
import MyCacheService from "./service"
|
||||
|
||||
export default {
|
||||
service: MyCacheService,
|
||||
}
|
||||
```
|
||||
|
||||
This exports the module's definition, indicating that the `MyCacheService` is the main service of the module.
|
||||
|
||||
---
|
||||
|
||||
## 4. Use Module
|
||||
|
||||
To use your Cache Module, add it to the `modules` object exported as part of the configurations in `medusa-config.js`. A Cache Module is added under the `cacheService` key.
|
||||
|
||||
For example:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
const modules = {
|
||||
// other modules...
|
||||
cacheService: {
|
||||
resolve: "./dist/modules/my-cache/index.ts",
|
||||
options: {
|
||||
// any necessary options
|
||||
ttl: 30,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Cache Module`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about what the Cache Module is.
|
||||
|
||||
## What is a Cache Module?
|
||||
|
||||
A Cache Module is used to cache the results of computations such as price selection or various tax calculations.
|
||||
|
||||
The underlying database, third-party service, or caching logic is flexible since it's implemented in a module. You can choose from Medusa’s cache modules or create your own to support something more suitable for your architecture.
|
||||
|
||||
<Note>
|
||||
|
||||
Refer to the Cache Modules reference for a list of Medusa’s Cache Modules.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Default Cache Module
|
||||
|
||||
By default, Medusa uses the In-Memory Cache Module. This module uses a plain JavaScript Map object to store the cache data.
|
||||
|
||||
This is useful for development. However, for production, it's highly recommended to use other Cache Modules, such as the [Redis Cache Module](!resources!/architectural-modules/cache/redis).
|
||||
@@ -0,0 +1,119 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} How to Create an Event Module`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to create an Event Module.
|
||||
|
||||
## 1. Create Module Directory
|
||||
|
||||
Start by creating a new directory for your module. For example, `src/modules/my-event`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Create the Event Service
|
||||
|
||||
Create the file `src/modules/my-event/service.ts` that holds the implementation of the event service.
|
||||
|
||||
The Event Module's main service must extend the `AbstractEventBusModuleService` class imported from `@medusajs/utils`:
|
||||
|
||||
```ts title="src/modules/my-event/service.ts"
|
||||
import { EmitData, Message } from "@medusajs/types"
|
||||
import { AbstractEventBusModuleService } from "@medusajs/utils"
|
||||
|
||||
class MyEventService extends AbstractEventBusModuleService {
|
||||
emit<T>(eventName: string, data: T, options: Record<string, unknown>): Promise<void>;
|
||||
emit<T>(data: EmitData<T>[]): Promise<void>;
|
||||
emit<T>(data: Message<T>[]): Promise<void>;
|
||||
emit(eventName: unknown, data?: unknown, options?: unknown): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
export default MyEventService
|
||||
```
|
||||
|
||||
The service implements the required methods based on the desired publish/subscribe logic.
|
||||
|
||||
### Note About the eventToSubscribersMap Property
|
||||
|
||||
The `AbstractEventBusModuleService` has a field `eventToSubscribersMap`, which is a JavaScript Map. The map's keys are the event names, whereas the value of each key is an array of subscribed handler functions.
|
||||
|
||||
In your custom implementation, you can use this property to manage the subscribed handler functions:
|
||||
|
||||
```ts
|
||||
const eventSubscribers =
|
||||
this.eventToSubscribersMap.get(eventName) || []
|
||||
```
|
||||
|
||||
### Implement emit Method
|
||||
|
||||
The `emit` method is used to push an event from the Medusa application into your messaging system. The subscribers to that event would then pick up the message and execute their asynchronous tasks.
|
||||
|
||||
The `emit` method has three different signatures:
|
||||
|
||||
1. The first signature accepts three parameters:
|
||||
- The first parameter is a string indicating the name of the event to trigger.
|
||||
- The second parameter is data to send to subscribers of that event.
|
||||
- The third optional parameter can be used to pass options specific to the event service.
|
||||
2. The second signature accepts one parameter, which is an array of objects having the following properties:
|
||||
- `eventName`: A string indicating the name of the event to trigger.
|
||||
- `data`: The data to send to subsribers of that event.
|
||||
- `options`: (optional) options specific to the event service.
|
||||
3. The third signature accepts one parameter, which is an array of objects having the following properties:
|
||||
- `eventName`: A string indicating the name of the event to trigger.
|
||||
- `body`: An object of event-related data. It has two properties: `data` holding the data of the event, and `metadata` which is an object with more details on how the event was emitted, such as the action that occurred or the service that emitted it.
|
||||
- `options`: (optional) options specific to the event service.
|
||||
|
||||
You can implement your method in a way that supports both signatures by checking the type of the first input. For example:
|
||||
|
||||
```ts title="src/modules/my-event/service.ts"
|
||||
class MyEventService extends AbstractEventBusModuleService {
|
||||
// ...
|
||||
emit<T>(eventName: string, data: T, options: Record<string, unknown>): Promise<void>;
|
||||
emit<T>(data: EmitData<T>[]): Promise<void>;
|
||||
emit<T>(data: Message<T>[]): Promise<void>;
|
||||
emit(eventOrData: unknown, data?: unknown, options?: unknown): Promise<void> {
|
||||
const isBulkEmit = Array.isArray(eventOrData)
|
||||
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Create Module Definition File
|
||||
|
||||
Create the file `src/modules/my-event/index.ts` with the following content:
|
||||
|
||||
```ts
|
||||
import MyEventService from "./service"
|
||||
|
||||
export default {
|
||||
service: MyEventService,
|
||||
}
|
||||
```
|
||||
|
||||
This exports the module's definition, indicating that the `MyEventService` is the main service of the module.
|
||||
|
||||
---
|
||||
|
||||
## 4. Use Module
|
||||
|
||||
To use your Event Module, add it to the `modules` object exported as part of the configurations in `medusa-config.js`. An Event Module is added under the `eventBus` key.
|
||||
|
||||
For example:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
const modules = {
|
||||
// other modules...
|
||||
eventBus: {
|
||||
resolve: "./dist/modules/my-event/index.ts",
|
||||
options: {
|
||||
// any necessary options
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Event Module`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about what the Event Module is.
|
||||
|
||||
## What is an Event Module?
|
||||
|
||||
In previous chapters, you learned that the Medusa application emits events, and subscribers handle those events.
|
||||
|
||||
The Event Module implements the underlying publish/subscribe system that handles queueing events, emitting them, and executing their subscribers.
|
||||
|
||||
This makes the event architecture customizable, as you can either choose one of Medusa’s Event Modules or create your own.
|
||||
|
||||
---
|
||||
|
||||
## Default Event Module
|
||||
|
||||
By default, Medusa uses the Local Event Module. This module uses Node’s EventEmitter to implement the publish/subscribe system.
|
||||
|
||||
This is useful for development. However, for production, it’s highly recommended to use other Event Modules, such as the [Redis Event Module](!resources!/architectural-modules/event/redis).
|
||||
@@ -0,0 +1,13 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Architectural Concepts`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you’ll learn more about customizable aspects of Medusa’s architecture.
|
||||
|
||||
By the end of these chapters, you’ll learn about:
|
||||
|
||||
- What an architectural module is.
|
||||
- The cache architectural module and how to create one.
|
||||
- The events architectural module and how to create one.
|
||||
@@ -0,0 +1,57 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Admin Customizations`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to customize the Medusa Admin dashboard.
|
||||
|
||||
<Note type="soon">
|
||||
|
||||
Admin customizations are coming soon.
|
||||
|
||||
</Note>
|
||||
|
||||
## Overview
|
||||
|
||||
The Medusa Admin is installed in your Medusa application and runs at port `7001` when you start the Medusa application.
|
||||
|
||||
You can extend or customize the Medusa Admin to add widgets or new pages that interact with API routes and provide admin users with custom functionalities.
|
||||
|
||||
---
|
||||
|
||||
## Example: Create a Widget
|
||||
|
||||
A widget is a React component that can be injected into an existing page in the admin dashboard.
|
||||
|
||||
For example, create the file `src/admin/widgets/product-widget.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx"
|
||||
import type { WidgetConfig } from "@medusajs/admin"
|
||||
|
||||
const ProductWidget = () => {
|
||||
return (
|
||||
<div>
|
||||
<h2>Product Widget</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const config: WidgetConfig = {
|
||||
zone: "product.details.after",
|
||||
}
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
This inserts a widget with the text “Product Widget” at the end of a product’s details page.
|
||||
|
||||
### Test the Widget
|
||||
|
||||
To test out the widget, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, open a product’s details page. You’ll find your custom widget at the bottom of the page.
|
||||
@@ -0,0 +1,63 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} API Routes`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn what API Routes are and how to create them.
|
||||
|
||||
## What is an API Route?
|
||||
|
||||
An API Route is a REST API endpoint. It exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems.
|
||||
|
||||
The Medusa core application provides a set of admin and store API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities.
|
||||
|
||||
---
|
||||
|
||||
## How to Create an API Route
|
||||
|
||||
An API Route is created in a TypeScript or JavaScript file under the `/src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`.
|
||||
|
||||
Each route file holds API Route handler functions for each HTTP method (`GET`, `POST`, `DELETE`, etc…).
|
||||
|
||||
For example, to create a `GET` API Route at `/store/hello-world`, create the file `src/api/store/hello-world/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/store/hello-world/route.ts"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const GET = (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: "[GET] Hello world!",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Test API Route
|
||||
|
||||
To test the API route above, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, send a `GET` request to the `/store/hello-world` API Route:
|
||||
|
||||
```bash apiTesting testApiUrl="http://localhost:9000/store/hello-world" testApiMethod="GET"
|
||||
curl http://localhost:9000/store/hello-world
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
<Note title="Use API routes when" type="success">
|
||||
|
||||
- You're exposing custom functionality to be used in the admin dashboard or an external application, such as a storefront.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,48 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Commerce Modules`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about Medusa's commerce modules.
|
||||
|
||||
## What is a Commerce Module?
|
||||
|
||||
Medusa provides all its commerce features as separate modules, such as the Product Module, Cart Module, or Order Module. These modules and your custom modules are interchangeable in the Medusa application, making Medusa’s architecture more flexible.
|
||||
|
||||
Refer to [this reference](!resources!/commerce-modules) for a full list of commerce modules in Medusa.
|
||||
|
||||
---
|
||||
|
||||
## Resolve Commerce Module Services
|
||||
|
||||
Similarly to your custom module, a commerce module's main service is registered in the Medusa container. So, you can resolve it in your resources, such as API route, to use its functionality.
|
||||
|
||||
For example, you saw this code snippet in the [Medusa container chapter](../medusa-container/page.mdx):
|
||||
|
||||
```ts highlights={[["13"]]}
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const productModuleService: IProductModuleService =
|
||||
req.scope.resolve(ModuleRegistrationName.PRODUCT)
|
||||
|
||||
const [, count] = await productModuleService.listAndCount()
|
||||
|
||||
res.json({
|
||||
count,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
When you resolve the `ModuleRegistrationName.PRODUCT` (or `productModuleService`) registration name, you're actually resolving the main service of the Product Module.
|
||||
|
||||
To resolve the main service of any commerce module, use the `ModuleRegistrationName` enum imported from `@medusajs/modules-sdk` to refer to its registration name in the Medusa container.
|
||||
@@ -0,0 +1,228 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Data Models`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn what data models are and how to create a data model.
|
||||
|
||||
## What is a Data Model?
|
||||
|
||||
A data model is a class that represents a table in the database. A data model is created in a module. You can then create a service that manages that data model.
|
||||
|
||||
Data models are based on [MikroORM](https://mikro-orm.io/docs/quick-start). So, you can use its decorators, types, and utilities when creating a model.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Data Model
|
||||
|
||||
<Note title="Steps Summary">
|
||||
|
||||
1. Create data model class in a module.
|
||||
2. Generate migration for the data model.
|
||||
3. Add migration scripts to the module's definition.
|
||||
4. Run migration to add table for data model in the database.
|
||||
|
||||
</Note>
|
||||
|
||||
A data model is a class created in a TypeScript or JavaScript file created in a module under its `models` directory.
|
||||
|
||||
For example, create the file `src/modules/hello/models/my-custom.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/models/my-custom.ts"
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
@Entity()
|
||||
export class MyCustom {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
name: string
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "mc")
|
||||
}
|
||||
|
||||
@OnInit()
|
||||
OnInit() {
|
||||
this.id = generateEntityId(this.id, "mc")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This defines a new data model `MyCustom` with the fields `id` and `name`.
|
||||
|
||||
The `onCreate` method generates an ID for the data model's record when it's created. The `onInit` method sets the ID when the record is loaded.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
The `generateEntityId` utility method prefixes the `id` of a record with the string provided in the second parameter. This follows Medusa's conventions of creating IDs.
|
||||
|
||||
</Note>
|
||||
|
||||
### Create a Migration
|
||||
|
||||
After creating the data model, you must create a migration that creates a table in your database for this data model.
|
||||
|
||||
A migration is a class that implements an `up` and `down` method, where the `up` method reflects changes on the database, and the `down` method reverts the changes from the database.
|
||||
|
||||
<Details summaryContent="Generate with MikroORM">
|
||||
MikroORM provides a CLI tool that helps you generate migrations. To use it:
|
||||
|
||||
1. Create the file `src/modules/hello/mikro-orm.config.dev.ts` with the following content:
|
||||
|
||||
```ts
|
||||
import "dotenv/config"
|
||||
import path from "path"
|
||||
import { TSMigrationGenerator } from "@medusajs/utils"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
|
||||
module.exports = {
|
||||
entities: [MyCustom],
|
||||
schema: "public",
|
||||
clientUrl: "postgres://postgres@localhost/medusa-hello",
|
||||
type: "postgresql",
|
||||
migrations: {
|
||||
path: path.join(__dirname, "migrations"),
|
||||
generator: TSMigrationGenerator,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
2. Run the following command in the root directory of your Medusa application:
|
||||
|
||||
```bash
|
||||
npx cross-env MIKRO_ORM_CLI=./src/modules/hello/mikro-orm.config.dev.ts mikro-orm migration:create
|
||||
```
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
You can add this command as a script in `package.json` for easy usage in the future. Use this command whenever you want to generate a new migration in your module.
|
||||
|
||||
</Note>
|
||||
|
||||
After running the command, a new file is created under the `src/modules/hello/migrations` directory. This file holds `up` and `down` methods that define the actions to execute when running and reverting the migration respectively.
|
||||
|
||||
</Details>
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20240429090012 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql("create table if not exists \"my_custom\" (\"id\" varchar(255) not null, \"name\" text not null, constraint \"my_custom_pkey\" primary key (\"id\"));")
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql("drop table if exists \"my_custom\" cascade;")
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
In the `up` method, you create the table `my_custom` and define its columns. In the `down` method, you drop the table.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
The queries performed in each of the methods use PostgreSQL syntax.
|
||||
|
||||
</Note>
|
||||
|
||||
### Add Migration to Module Definition
|
||||
|
||||
After creating the migration, you must add it to your module's definition. Otherwise, the Medusa application won't run it.
|
||||
|
||||
To add a module's migrations to its definitions, use the `ModulesSdkUtils` utility functions imported from `@medusajs/utils`. It has functions to create and define the migration scripts in your module definition.
|
||||
|
||||
Change the content of `src/modules/hello.index.ts` that you created in a [previous chapter](../modules-and-services/page.mdx) to the following:
|
||||
|
||||
```ts title="src/modules/hello.index.ts" highlights={[["2"], ["6"], ["10"], ["12"], ["14"], ["18"], ["22"], ["28"], ["36"]]}
|
||||
import HelloModuleService from "./service"
|
||||
// add necessary imports
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
|
||||
// define useful constants
|
||||
const moduleName = "hello"
|
||||
const pathToMigrations = __dirname + "/migrations"
|
||||
|
||||
// assemble object to pass to utility functions
|
||||
const migrationScriptOptions = {
|
||||
// the module's name
|
||||
moduleName,
|
||||
// the data models of the modules
|
||||
models: {
|
||||
MyCustom,
|
||||
},
|
||||
// the path to the migrations directory
|
||||
pathToMigrations,
|
||||
}
|
||||
|
||||
// create and export the script that runs migrations
|
||||
export const runMigrations = ModulesSdkUtils
|
||||
.buildMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
|
||||
// create and export the script that reverts migrations
|
||||
export const revertMigration = ModulesSdkUtils
|
||||
.buildRevertMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
|
||||
export default {
|
||||
service: HelloModuleService,
|
||||
// add the run and revert migration scripts to the module's definition
|
||||
runMigrations,
|
||||
revertMigration,
|
||||
}
|
||||
```
|
||||
|
||||
After importing `ModulesSdkUtils`, you use its `buildMigrationScript` function to create the script that runs the migration, and its `buildRevertMigrationScript` function to create the script that reverts the migration.
|
||||
|
||||
Both the `buildMigrationScript` and `buildRevertMigrationScript` accept the same object type as a parameter, which has the following properties:
|
||||
|
||||
- `moduleName`: The name of the module that the migrations belong to.
|
||||
- `models`: An object of the module's data models.
|
||||
- `pathToMigrations`: The path to the `migrations` directory.
|
||||
|
||||
Both created scripts must be exported in the file and within the module's definition object.
|
||||
|
||||
### Run Migration
|
||||
|
||||
To reflect the changes in the migration, transpile your source files using the `build` command, then run the `migration` command:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
npx medusa migrations run
|
||||
```
|
||||
|
||||
If ran successfully, the `my_custom` table will be created in the database.
|
||||
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
<Note title="Use data models when" type="success">
|
||||
|
||||
- You want to store data related to your customization in the database.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Don't use data models if" type="error">
|
||||
|
||||
- You want to store simple key-value pairs related to an existing data model. Instead, use the `metadata` field that most existing models have, which is an object of custom key-value pairs.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,116 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Events and Subscribers`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to handle events with subscribers.
|
||||
|
||||
## What is an Event?
|
||||
|
||||
When an action is performed in Medusa, such as creating a product, the Medusa application emits an event. You can listen to those events and perform an asynchronous action.
|
||||
|
||||
---
|
||||
|
||||
## What is a Subscriber?
|
||||
|
||||
A subscriber is a function executed whenever its associated event is emitted.
|
||||
|
||||
The subscriber is created in a TypeScript or JavaScript file under the `src/subscribers` directory.
|
||||
|
||||
For example, create the file `src/subscribers/product-created.ts` with the following content:
|
||||
|
||||
```ts title="src/subscribers/product-created.ts"
|
||||
import {
|
||||
ProductService,
|
||||
type SubscriberConfig,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
// subscriber function
|
||||
export default async function productCreateHandler() {
|
||||
console.log("A product was created")
|
||||
}
|
||||
|
||||
// subscriber config
|
||||
export const config: SubscriberConfig = {
|
||||
event: ProductService.Events.CREATED,
|
||||
}
|
||||
```
|
||||
|
||||
A subscriber file must export:
|
||||
|
||||
- The subscriber function which is an asynchronous function executed whenever the associated event is triggered.
|
||||
- A configuration object defining the event this subscriber is listening to.
|
||||
|
||||
The above subscriber listens to the `ProductService.Events.CREATED` (`product-created`) event. Whenever the event is emitted, it logs in the terminal `A product is created`.
|
||||
|
||||
### Test Subscriber
|
||||
|
||||
To test the subscriber, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, create a product using the Medusa Admin dashboard. Afterward, you’ll see the following messages logged in the terminal:
|
||||
|
||||
```bash
|
||||
info: Processing product.created which has 1 subscribers
|
||||
A product is created
|
||||
```
|
||||
|
||||
The first message indicates that the `product.created` event was emitted, and the second one is the message logged from the subscriber.
|
||||
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
<Note title="Use subscribers when" type="success">
|
||||
|
||||
- You want to perform an action everytime a specific event is emitted in the Medusa application.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Resolve Resources
|
||||
|
||||
The subscriber function accepts an object parameter with the property `container`. Its value is the Medusa container, and you can use it to resolve other resources, such as services.
|
||||
|
||||
For example:
|
||||
|
||||
{/* TODO change how event names are loaded */}
|
||||
|
||||
export const highlights = [
|
||||
["11"],
|
||||
["14", "resolve", "Resolve the `IProductModuleService`."],
|
||||
["14", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."]
|
||||
]
|
||||
|
||||
```ts title="src/subscribers/product-created.ts" highlights={highlights}
|
||||
import {
|
||||
ProductService,
|
||||
SubscriberArgs,
|
||||
type SubscriberConfig,
|
||||
} from "@medusajs/medusa"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
|
||||
export default async function productCreateHandler({
|
||||
data,
|
||||
container,
|
||||
}: SubscriberArgs<Record<string, string>>) {
|
||||
const productModuleService: IProductModuleService =
|
||||
container.resolve(ModuleRegistrationName.PRODUCT)
|
||||
|
||||
const product = await productModuleService.retrieve(data.id)
|
||||
|
||||
console.log(`The product ${product.title} was created`)
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: ProductService.Events.CREATED,
|
||||
}
|
||||
```
|
||||
|
||||
You use the container to resolve the `IProductModuleService`, then log the title of the created product.
|
||||
@@ -0,0 +1,22 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Important Directories and Files`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about important directories and files in your Medusa application's codebase.
|
||||
|
||||
## src
|
||||
|
||||
This directory is the central place for your custom development. It includes the following sub-directories:
|
||||
|
||||
- `admin`: Holds your admin dashboard's custom components and pages.
|
||||
- `api`: Holds your custom API routes that are added as endpoints in your Medusa application.
|
||||
- `jobs`: Holds your scheduled jobs that run at a specified interval during your Medusa application's runtime.
|
||||
- `loaders`: Holds your loaders that run at the Medusa application's start-up.
|
||||
- `modules`: Holds your custom modules that implement custom business logic.
|
||||
- `subscribers`: Holds your event listeners that are executed asynchronously whenever an event is emitted.
|
||||
|
||||
## medusa-config.js
|
||||
|
||||
This file holds your Medusa configurations, such as your PostgreSQL database configurations or your Medusa Admin plugin configurations.
|
||||
@@ -0,0 +1,87 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Loaders`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about loaders and how to use them.
|
||||
|
||||
## What is a Loader?
|
||||
|
||||
A loader is a function executed when the Medusa application starts.
|
||||
|
||||
A loader can be created either within a module or in the `src/loaders` directory.
|
||||
|
||||
<Note>
|
||||
|
||||
This guide explains the first approach. This second approach will be explained in later chapters.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Loader?
|
||||
|
||||
A loader is created in a TypeScript or JavaScript file under the `loaders` directory of a module. The file must export the loader function.
|
||||
|
||||
For example, create the file `src/modules/hello/loaders/hello-world.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/loaders/hello-world.ts"
|
||||
export default function helloWorldLoader() {
|
||||
console.log(
|
||||
"[HELLO MODULE] Just started the Medusa application!"
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Export Loader in Module Definition
|
||||
|
||||
You must export your loaders in the module definition. Otherwise, your Medusa application won't run them.
|
||||
|
||||
To do that, import the loader in `src/modules/hello/index.ts` and export it in the module's definition:
|
||||
|
||||
```ts title="src/modules/hello/index.ts"
|
||||
// other imports...
|
||||
import helloWorldLoader from "./loaders/hello-world"
|
||||
|
||||
// ...
|
||||
|
||||
export default {
|
||||
// ...
|
||||
loaders: [helloWorldLoader],
|
||||
}
|
||||
```
|
||||
|
||||
The value of the `loaders` property of the module definition is an array of loader functions.
|
||||
|
||||
### Test Loader
|
||||
|
||||
Start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Among the messages logged in the terminal, you’ll see the following message:
|
||||
|
||||
```bash
|
||||
[HELLO MODULE] Just started the Medusa application!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
<Note title="Use loaders when" type="success">
|
||||
|
||||
- You're performing an action at application start-up.
|
||||
- You're establishing a one-time connection with an external system.
|
||||
- You're registering a custom resource in the container.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Don't use loaders if" type="error">
|
||||
|
||||
- You want to perform an action continuously or at a set time pattern in the application. Use scheduled jobs instead, which is explained in the next chapter.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,43 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Medusa container`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about Medusa’s Medusa container and how to use it.
|
||||
|
||||
## What is the Medusa container?
|
||||
|
||||
The Medusa container holds all registered resources in the Medusa application. You use it to resolve resources, such as services.
|
||||
|
||||
For example, in a custom API route you can resolve any service registered in the Medusa application using the `scope.resolve` method of the `MedusaRequest` parameter:
|
||||
|
||||
export const highlights = [
|
||||
["13", "resolve", "Resolve the `IProductModuleService`"],
|
||||
["13", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."]
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const productModuleService: IProductModuleService =
|
||||
req.scope.resolve(ModuleRegistrationName.PRODUCT)
|
||||
|
||||
const [, count] = await productModuleService.listAndCount()
|
||||
|
||||
res.json({
|
||||
count,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This route handler function resolves the `IProductModuleService` and uses it to return the full count of products in the Medusa application.
|
||||
@@ -0,0 +1,138 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Modules and Services`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about modules, their main service, and how to create them.
|
||||
|
||||
## What is a Module?
|
||||
|
||||
A module is a package of reusable functionalities that can be integrated into your Medusa application without affecting the overall system.
|
||||
|
||||
All commerce and architectural customizations and development start with creating a module.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Module
|
||||
|
||||
<Note title="Steps Summary">
|
||||
|
||||
1. Create module's main service.
|
||||
2. Create module definition.
|
||||
3. Add module to Medusa's configurations.
|
||||
|
||||
</Note>
|
||||
|
||||
Modules are created in a sub-directory of `src/modules`. The directory name is the camel-case name of the module.
|
||||
|
||||
For example, create the directory `src/modules/hello`.
|
||||
|
||||
### 1. Create a Service
|
||||
|
||||
A module must defines a service. A service is a TypeScript or JavaScript class holding methods related to a business logic or commerce functionality.
|
||||
|
||||
For example, create the service `src/modules/hello/service.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/service.ts"
|
||||
export default class HelloModuleService {
|
||||
getMessage() {
|
||||
return "Hello, world!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When the module is loaded in the Medusa application, its main service is registered in the Medusa container.
|
||||
|
||||
### 2. Export Module Definition
|
||||
|
||||
A module must have an `index.ts` file in its root directory that exports its definition. The definition specifies the main service of the module.
|
||||
|
||||
For example, create the file `src/modules/hello.index.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello.index.ts" highlights={[["4", "", "The main service of the module."]]}
|
||||
import HelloModuleService from "./service"
|
||||
|
||||
export default {
|
||||
service: HelloModuleService,
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Register Module in Configurations
|
||||
|
||||
The last step is to register the module in Medusa’s configurations.
|
||||
|
||||
In `medusa-config.js`, add the module to the `modules` object:
|
||||
|
||||
```js title="medusa-config.js" highlights={[["2", "helloModuleService", "The key of the main service to be registered in the Medusa container."]]}
|
||||
const modules = {
|
||||
helloModuleService: {
|
||||
resolve: "./dist/modules/hello",
|
||||
},
|
||||
// other modules...
|
||||
}
|
||||
```
|
||||
|
||||
The key (`helloModuleService`) is the name of the module’s main service to be registered in the Medusa container. Its value is an object having the `resolve` property. `resolve` ’s value is either an `npm` package’s name or a path to the directory holding the module.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
When `resolve` points to a directory, it must point to the transpiled module (the directory the module is in after running the `build` command). Hence, the path in the example above points to the module in the `dist` directory.
|
||||
|
||||
</Note>
|
||||
|
||||
### 4. Test the Module
|
||||
|
||||
Since the module's main service is registered in the Medusa container, you can resolve it in other resources to use its functionalities.
|
||||
|
||||
One way to test the module is to create the API route `src/api/store/custom/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/store/custom/route.ts"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
|
||||
import HelloService from "../../../modules/hello/service"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloService = req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
res.json({
|
||||
message: helloModuleService.getMessage(),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Then, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Finally, send a `GET` request to `/store/custom`:
|
||||
|
||||
```bash apiTesting testApiUrl="http://localhost:9000/store/custom" testApiMethod="GET"
|
||||
curl http://locahost:9000/store/custom
|
||||
```
|
||||
|
||||
You’ll receive the following response:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Hello, world!"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use Modules
|
||||
|
||||
Use a module when you're implementing custom business logic or extending existing ones.
|
||||
|
||||
Some common use cases of when a module is useful:
|
||||
|
||||
- You're implementing a custom commerce feature. For example, you're implementing digital products.
|
||||
- You want to extend data models in other commerce modules, such as adding a field or a relation to the `Product` model.
|
||||
- You want to re-use your custom commerce functionalities across Medusa applications or use them in other environments, such as Edge functions and Next.js apps.
|
||||
@@ -0,0 +1,29 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Modules Directory Structure`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this document, you'll learn about the expected files and directories in your module.
|
||||
|
||||

|
||||
|
||||
## index.ts
|
||||
|
||||
The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in the [previous chapter](../modules-and-services/page.mdx).
|
||||
|
||||
---
|
||||
|
||||
## service.ts
|
||||
|
||||
A module must have a main service created in the `service.ts` file at the root of your module directory as explained in the [previous chapter](../modules-and-services/page.mdx).
|
||||
|
||||
---
|
||||
|
||||
## Other Directories
|
||||
|
||||
The following directories are optional and their content are explained more in the following chapters:
|
||||
|
||||
- `models`: Holds the data models representing tables in the database.
|
||||
- `migrations`: Holds the migration files used to reflect changes on the database.
|
||||
- `loaders`: Holds the scripts to run on the Medusa application's start-up.
|
||||
@@ -0,0 +1,20 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} The Basics`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you’ll learn about the basic concepts of Medusa. These concepts are central to your Medusa development.
|
||||
|
||||
By the end of these chapter, you’ll be able to:
|
||||
|
||||
- Expose your custom functionalities through REST APIs.
|
||||
- Create custom modules that define custom business logic.
|
||||
- Create custom data models.
|
||||
- Perform asynchronus actions when an event occurs.
|
||||
- Execute scripts when the Medusa application starts.
|
||||
- Create scheduled jobs that run at a specified pattern during the Medusa application's runtime.
|
||||
- Create flows as a series of steps involving multiple services.
|
||||
- Customize the admin dashboard to inject components on existing pages or add new pages.
|
||||
|
||||
You'll then use this knowledge to learn more about Medusa's modular architecture, and how to make customizations to core models and features.
|
||||
@@ -0,0 +1,143 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Scheduled Jobs`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about scheduled jobs and how to use them.
|
||||
|
||||
## What is a Scheduled Job?
|
||||
|
||||
A scheduled job is a function that is executed at a specified interval of time in the background of your Medusa application. It’s like a cron job that runs during the application's runtime.
|
||||
|
||||
For example, you can synchronize your inventory with an Enterprise Resource Planning (ERP) system once a day using a scheduled job.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Scheduled Job?
|
||||
|
||||
<Note type="check">
|
||||
|
||||
- [Redis](https://redis.io/) is installed and Redis server is running.
|
||||
|
||||
</Note>
|
||||
|
||||
A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory.
|
||||
|
||||
For example, create the file `src/jobs/hello-world.ts` with the following content:
|
||||
|
||||
```ts title="src/jobs/hello-world.ts"
|
||||
import { ScheduledJobConfig } from "@medusajs/medusa"
|
||||
|
||||
// the scheduled-job function
|
||||
export default function () {
|
||||
console.log("Time to say hello world!")
|
||||
}
|
||||
|
||||
// the job's configurations
|
||||
export const config: ScheduledJobConfig = {
|
||||
name: "hello-world",
|
||||
// execute every minute
|
||||
schedule: "* * * * *",
|
||||
}
|
||||
```
|
||||
|
||||
A scheduled job file must export:
|
||||
|
||||
- The function executed whenever it’s time to run the scheduled job.
|
||||
- A configuration object defining the job. It has two properties:
|
||||
- `name`: a unique name for the job.
|
||||
- `schedule`: a [cron expression](https://crontab.guru/).
|
||||
|
||||
This scheduled job executes every minute and logs into the terminal `Time to say hello world!`.
|
||||
|
||||
### Test Scheduled Jobs
|
||||
|
||||
To test out your scheduled job:
|
||||
|
||||
1. Uncomment the following line in the `medusa-config.js` file:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
const projectConfig = {
|
||||
// ...
|
||||
// Uncomment the following lines to enable REDIS
|
||||
redis_url: REDIS_URL,
|
||||
}
|
||||
```
|
||||
|
||||
2. Start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
After a minute, the following message will be logged to the terminal:
|
||||
|
||||
```bash
|
||||
info: Processing scheduled job: hello-world
|
||||
Time to say hello world!
|
||||
```
|
||||
|
||||
The first line indicates that the application is executing the scheduled job. The second line is the text logged by the scheduled job.
|
||||
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
<Note title="Use scheduled jobs when" type="success">
|
||||
|
||||
- You're executing an action at a specified time interval during application runtime.
|
||||
- The action must be executed automatically.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Don't use scheduled jobs if" type="error">
|
||||
|
||||
- You want the action to execute at a specified time interval while the Medusa application isn't running. Instead, use the operating system's equivalent of a cron job.
|
||||
- You want to execute the action once. Use loaders instead.
|
||||
- You want to execute the action if an event occurs. Use subscribers instead.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Resolve Resources
|
||||
|
||||
The scheduled job function receives an object parameter that has a `container` property. Its value is the Medusa container, which you can use to resolve resources in your Medusa application, such as services.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["12", "resolve", "Resolve the `IProductModuleService`."],
|
||||
["12", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."]
|
||||
]
|
||||
|
||||
```ts title="src/jobs/hello-world.ts" highlights={highlights}
|
||||
import {
|
||||
ScheduledJobArgs,
|
||||
ScheduledJobConfig,
|
||||
} from "@medusajs/medusa"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
|
||||
export default async function ({
|
||||
container,
|
||||
}: ScheduledJobArgs) {
|
||||
const productModuleService: IProductModuleService =
|
||||
container.resolve(ModuleRegistrationName.PRODUCT)
|
||||
|
||||
const [, count] = await productModuleService.listAndCount()
|
||||
|
||||
console.log(
|
||||
`Time to check products! You have ${count} product(s)`
|
||||
)
|
||||
}
|
||||
|
||||
export const config: ScheduledJobConfig = {
|
||||
name: "hello-world",
|
||||
// execute every minute
|
||||
schedule: "* * * * *",
|
||||
}
|
||||
```
|
||||
|
||||
In the scheduled job function, you resolve the `IProductModuleService` and retrieve the number of products in the store, then log the number in the terminal.
|
||||
@@ -0,0 +1,77 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Services`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn what services are and how to create a service.
|
||||
|
||||
## What is a Service?
|
||||
|
||||
A service is a class that holds methods related to a business logic or commerce functionality. For example, the `ProductService` holds methods to retrieve and manage products.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Service
|
||||
|
||||
Services are created in a TypeScript or JavaScript file under the `src/services` directory of your Medusa application.
|
||||
|
||||
For example, to create a `HelloWorld` service, create the file `src/services/hello-world.ts` with the following content:
|
||||
|
||||
```ts title="src/services/hello-world.ts"
|
||||
class HelloWorld {
|
||||
// TODO add methods
|
||||
}
|
||||
|
||||
export default HelloWorld
|
||||
```
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
A service's file name must be the kebab-case name of the service without the word `Service`.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
<Note title="Use services when" type="success">
|
||||
|
||||
- You're implementing custom commerce functionality to be reused across the Medusa application.
|
||||
- You're integrating a third-party service, such as an ERP system.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Resolve Resources
|
||||
|
||||
In a service's constructor, you can resolve other services or resources of your Medusa application using the Medusa container.
|
||||
|
||||
For example, you can resolve the `IProductModuleService` to use it in your methods:
|
||||
|
||||
```ts title="src/services/hello-world.ts" highlights={[["10"]]}
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productModuleService: IProductModuleService
|
||||
}
|
||||
|
||||
class HelloWorldService {
|
||||
protected productModuleService: IProductModuleService
|
||||
|
||||
constructor({ productModuleService }: InjectedDependencies) {
|
||||
this.productModuleService = productModuleService
|
||||
}
|
||||
|
||||
async getMessage(): Promise<string> {
|
||||
const [, count] =
|
||||
await this.productModuleService.listAndCount()
|
||||
|
||||
return `You have ${count} products`
|
||||
}
|
||||
}
|
||||
|
||||
export default HelloWorldService
|
||||
```
|
||||
@@ -0,0 +1,268 @@
|
||||
import { CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Workflows`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about workflows and how to define and execute them.
|
||||
|
||||
## What is a Workflow?
|
||||
|
||||
A workflow is a series of queries and actions that complete a task.
|
||||
|
||||
You construct a Workflow similar to how you create a JavaScript function, but unlike regular functions, a Medusa Workflow creates an internal representation of your steps. This makes it possible to keep track of your Workflow’s progress, automatically retry failing steps, and, if necessary, roll back steps.
|
||||
|
||||
---
|
||||
|
||||
## How to Create and Execute a Workflow
|
||||
|
||||
### 1. Create the Steps
|
||||
|
||||
A workflow is made of a series of steps. A step is created using the `createStep` utility function.
|
||||
|
||||
Create the file `src/workflows/hello-world.ts` with the following content:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
|
||||
const step1 = createStep("step-1", async () => {
|
||||
return new StepResponse(`Hello from step one!`)
|
||||
})
|
||||
```
|
||||
|
||||
This creates one step that returns a hello message.
|
||||
|
||||
Steps can accepts input parameters. For example:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts"
|
||||
type WorkflowInput = {
|
||||
name: string
|
||||
}
|
||||
|
||||
const step2 = createStep(
|
||||
"step-2",
|
||||
async ({ name }: WorkflowInput) => {
|
||||
return new StepResponse(`Hello ${name} from step two!`)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Create a Workflow
|
||||
|
||||
Next, create the workflow using the `createWorkflow` function:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts"
|
||||
import {
|
||||
// other imports...
|
||||
createWorkflow,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
|
||||
// ...
|
||||
|
||||
type WorkflowOutput = {
|
||||
message: string
|
||||
}
|
||||
|
||||
const myWorkflow = createWorkflow<
|
||||
WorkflowInput,
|
||||
WorkflowOutput
|
||||
>("hello-world", function (input) {
|
||||
const str1 = step1()
|
||||
// to pass input
|
||||
const str2 = step2(input)
|
||||
|
||||
return {
|
||||
message: str1,
|
||||
}
|
||||
})
|
||||
|
||||
export default myWorkflow
|
||||
|
||||
```
|
||||
|
||||
This creates a `hello-world` workflow. When you create a workflow, it’s constructed but not executed yet.
|
||||
|
||||
### 3. Execute the Workflow
|
||||
|
||||
You can execute a workflow from different places within Medusa.
|
||||
|
||||
- Use API Routes to execute the workflow in response to an API request or a webhook.
|
||||
- Use Subscribers to execute a workflow when an event is triggered.
|
||||
- Use Scheduled Jobs to execute a workflow on a regular schedule.
|
||||
|
||||
To execute the workflow, invoke it passing the Medusa container as a parameter, then use its run method:
|
||||
|
||||
<CodeTabs group="resource-types">
|
||||
<CodeTab label="API Route" value="api-route">
|
||||
|
||||
```ts title="src/api/store/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]}
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import myWorkflow from "../../../workflows/hello-world"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) {
|
||||
const { result } = await myWorkflow(req.scope)
|
||||
.run({
|
||||
input: {
|
||||
name: req.query.name as string,
|
||||
},
|
||||
})
|
||||
|
||||
res.send(result)
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="Subscriber" value="subscriber">
|
||||
|
||||
```ts title="src/subscribers/customer-created.ts" highlights={[["13"], ["14"], ["15"], ["16"], ["17"], ["18"]]}
|
||||
import {
|
||||
type SubscriberConfig,
|
||||
type SubscriberArgs,
|
||||
CustomerService,
|
||||
Customer,
|
||||
} from "@medusajs/medusa"
|
||||
import myWorkflow from "../workflows/hello-world"
|
||||
|
||||
export default async function handleCustomerCreate({
|
||||
data,
|
||||
container,
|
||||
}: SubscriberArgs<Customer>) {
|
||||
const { result } = await myWorkflow(container)
|
||||
.run({
|
||||
input: {
|
||||
name: data.first_name,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(result)
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: CustomerService.Events.CREATED,
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="Scheduled Job" value="scheduled-job">
|
||||
|
||||
```ts title="src/jobs/message-daily.ts" highlights={[["10"], ["11"], ["12"], ["13"], ["14"], ["15"]]}
|
||||
import {
|
||||
type ScheduledJobConfig,
|
||||
type ScheduledJobArgs,
|
||||
} from "@medusajs/medusa"
|
||||
import myWorkflow from "../workflows/hello-world"
|
||||
|
||||
export default async function handler({
|
||||
container,
|
||||
}: ScheduledJobArgs) {
|
||||
const { result } = await myWorkflow(container)
|
||||
.run({
|
||||
input: {
|
||||
name: "John",
|
||||
},
|
||||
})
|
||||
|
||||
console.log(result.message)
|
||||
}
|
||||
|
||||
export const config: ScheduledJobConfig = {
|
||||
name: "run-once-a-day",
|
||||
schedule: `0 0 * * *`,
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
### 4. Test Workflow
|
||||
|
||||
To test out your workflow, start your Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, send a `GET` request to `/store/workflow`:
|
||||
|
||||
```bash apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/workflows"
|
||||
curl http://localhost:9000/store/workflow
|
||||
```
|
||||
|
||||
You’ll receive the following response:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Hello from step one!"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
<Note title="Use workflows when" type="success">
|
||||
|
||||
- You're defining a flow with interactions across multiple systems and services.
|
||||
- You're defining flows to be used across different resources. For example, if you want to invoke the flow manually through an API Router, but also want to automate its running through a scheduled job.
|
||||
- You want to define how the series of actions are rolled-back when an error occurs.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Resolve Resources
|
||||
|
||||
Each step in the workflow receives as a second parameter a `context` object. The object holds a `container` property which is the Medusa container. You can use it to resolve other resources, such as services, of your Medusa application.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["14", "resolve", "Resolve the `IProductModuleService`."],
|
||||
["14", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."]
|
||||
]
|
||||
|
||||
```ts title="src/workflows/product-count.ts" highlights={highlights}
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
createWorkflow,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
|
||||
type WorkflowOutput = {
|
||||
count: number
|
||||
}
|
||||
|
||||
const step1 = createStep("step-1", async (_, context) => {
|
||||
const productModuleService: IProductModuleService =
|
||||
context.container.resolve(ModuleRegistrationName.PRODUCT)
|
||||
return new StepResponse(
|
||||
(await productModuleService.listAndCount())[1]
|
||||
)
|
||||
})
|
||||
|
||||
const myWorkflow = createWorkflow<unknown, WorkflowOutput>(
|
||||
"product-count",
|
||||
function () {
|
||||
const count = step1()
|
||||
|
||||
return {
|
||||
count,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export default myWorkflow
|
||||
```
|
||||
@@ -0,0 +1,172 @@
|
||||
import { Table } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Cheat sheet`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
This chapter provides a cheat sheet for Medusa's resources on when to use or not use them.
|
||||
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Name</Table.HeaderCell>
|
||||
<Table.HeaderCell>Use if</Table.HeaderCell>
|
||||
<Table.HeaderCell>Don't use if</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body className="[&_td]:align-top [&_td]:pt-1 [&_td:first-child]:font-bold">
|
||||
<Table.Row>
|
||||
<Table.Cell>API Routes</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
- You're exposing custom functionality to be used in the admin dashboard or an external application, such as a storefront.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
\-
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>Modules</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
- You're implementing custom commerce functionalities to be reused across Medusa applications.
|
||||
- You're implementing custom architectural business logic.
|
||||
- You're adding extending another module's data model to add fields to it, such as the `Product` model.
|
||||
- You're completely replacing an existing commerce module to change how a certain functionality is implemented in the Medusa application.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
\-
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>Data Models</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
- You want to store data related to your customization in the database.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
- You want to store simple key-value pairs related to a Medusa data model. Instead, use the `metadata` field that models have, which is an object of custom key-value pairs.
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>Loaders</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
- You're performing an action at application start-up.
|
||||
- You're establishing a one-time connection with an external system.
|
||||
- You're registering a custom resource in the container.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
- You want to perform an action continuously or at a set time pattern in the application. Use scheduled jobs instead.
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>Subscribers</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
- You want to perform an action everytime a specific event is emitted in the Medusa application.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
\-
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>Scheduled Jobs</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
- You're executing an action at a specified time interval during application runtime.
|
||||
- The action must be executed automatically.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
- You want the action to execute at a specified time interval while the Medusa application isn't running. Instead, use the operating system's equivalent of a cron job.
|
||||
- You want to execute the action once. Use loaders instead.
|
||||
- You want to execute the action if an event occurs. Use subscribers instead.
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>Workflows</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
- You're defining a flow with interactions across multiple systems and services.
|
||||
- You're defining flows to be used across different resources. For example, if you want to invoke the flow manually through an API Router, but also want to automate its running through a scheduled job.
|
||||
- You want to define how the series of actions are rolled-back when an error occurs.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
\-
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>Middlewares</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
- You want to guard API routes by a custom condition.
|
||||
- You're modifying the request body.
|
||||
- You're registering custom resources in the Medusa container.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
\-
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>Batch Jobs</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
- You’re implementing an asynchronous job that’s triggered manually.
|
||||
- You're tracking the status of the asynchronous job.
|
||||
- You're keeping track of all batch job executions, which are stored in the database.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
- You want to trigger the asynchronous job automatically. Instead, use scheduled jobs. You can also create the batch job in a scheduled job.
|
||||
- You want the task to be performed and finished before consecutive tasks. Instead, use a service.
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>Plugins</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
- You're integrating a third-party service into your Medusa application.
|
||||
- You're creating reusable Medusa application customizations.
|
||||
- You're providing features that are specific to a Medusa application, such as a migration tool from another platform to Medusa.
|
||||
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
|
||||
- You want to run the plugin in isolation.
|
||||
- You want to link custom data models to those in Medusa's commerce modules.
|
||||
- You want to make architectural changes.
|
||||
|
||||
For all of the cases above, use a Module.
|
||||
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
@@ -0,0 +1,88 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Feature Flags`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about feature flags and how Medusa uses them.
|
||||
|
||||
## What is a Feature Flag?
|
||||
|
||||
A feature flag is a configuration that toggles beta features in Medusa.
|
||||
|
||||
A beta feature may be disabled but is still part of the published `@medusajs/medusa` package. This allows our team to release new features rapidly and continuously without impacting Medusa applications used in production.
|
||||
|
||||
Developers interested in testing a new feature can enable its flag through Medusa’s configurations.
|
||||
|
||||
<Note type="warning">
|
||||
|
||||
Enabling a feature flag isn’t recommended for Medusa applications in a production environment, as it can cause unexpected errors and issues.
|
||||
|
||||
</Note>
|
||||
|
||||
{/* TODO re-add this based on whether we have feature flags (after V2). */}
|
||||
|
||||
{/* ---
|
||||
|
||||
## List of Feature Flags
|
||||
|
||||
Refer to this reference for a full list of feature flags. */}
|
||||
|
||||
---
|
||||
|
||||
## How to Toggle a Feature Flag
|
||||
|
||||
### Option 1: Using an Environment Variable
|
||||
|
||||
A feature flag has an associated environment variable that, if you set it, it toggles the feature flag.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
MEDUSA_FF_TAX_INCLUSIVE_PRICING=true
|
||||
```
|
||||
|
||||
### Option 2: Using Medusa Configurations
|
||||
|
||||
A feature flag also has an associated key that can be used to toggle its value in Medusa’s configurations.
|
||||
|
||||
The configuration object exported in `medusa-config.js` has a `featureFlags` property. Its value is an object where each key is the key of a feature flag, and its value is a boolean indicating whether to enable or disable the flag.
|
||||
|
||||
For example:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
module.exports = {
|
||||
featureFlags: {
|
||||
tax_inclusive_pricing: true,
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
If you’ve set both the environment variable and the feature flag key in the configurations, the environment variable’s value has a higher precedence.
|
||||
|
||||
</Note>
|
||||
|
||||
### Run Migrations
|
||||
|
||||
A feature guarded by a flag may require changes in the database.
|
||||
|
||||
So, after enabling a feature flag, it’s recommended to run migrations in your Medusa application:
|
||||
|
||||
```bash
|
||||
npx medusa migrations run
|
||||
```
|
||||
|
||||
If you disable the feature flag, run the following command to revert migrations:
|
||||
|
||||
```bash
|
||||
npx medusa migrations revert
|
||||
```
|
||||
|
||||
<Note>
|
||||
|
||||
This reverts the last migrations, so if you ran other migrations since you toggled the feature flag, it’ll revert them instead. You can run it multiple times to revert the last migrations.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,148 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Logging`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to use Medusa’s logging utility.
|
||||
|
||||
## Logger Class
|
||||
|
||||
Medusa provides a `Logger` class that provides advanced logging functionalities. This includes configuring logging levels or saving logs to a file.
|
||||
|
||||
The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`.
|
||||
|
||||
---
|
||||
|
||||
## How to Log a Message
|
||||
|
||||
Resolve the `logger` using the Medusa container to log a message in your resource.
|
||||
|
||||
For example, create the file `src/loaders/log-message.ts` with the following content:
|
||||
|
||||
export const highlights = [
|
||||
["4", "resolve", "Resolve the `Logger` class."],
|
||||
["6", "info", "Log a message of level `info`."]
|
||||
]
|
||||
|
||||
```ts title="src/loaders/log-message.ts" highlights={highlights}
|
||||
import { Logger, MedusaContainer } from "@medusajs/medusa"
|
||||
|
||||
export default async (container: MedusaContainer) => {
|
||||
const logger: Logger = container.resolve("logger")
|
||||
|
||||
logger.info("I'm using the logger!")
|
||||
}
|
||||
```
|
||||
|
||||
This creates a loader that resolves the `logger` from the Medusa container and uses it to log a message.
|
||||
|
||||
### Test the Loader
|
||||
|
||||
To test out the above loader, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
You’ll see the following message as part of the logged messages:
|
||||
|
||||
```text
|
||||
info: I'm using the logger!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Log Levels
|
||||
|
||||
The `Logger` class has the following methods:
|
||||
|
||||
- `info`: The message is logged with level `info`.
|
||||
- `warn`: The message is logged with level `warn`.
|
||||
- `error`: The message is logged with level `error`.
|
||||
- `debug`: The message is logged with level `debug`.
|
||||
|
||||
Each of these methods accepts a string parameter to log in the terminal with the associated level.
|
||||
|
||||
---
|
||||
|
||||
## Logging Configurations
|
||||
|
||||
### Log Level
|
||||
|
||||
The available log levels, from lowest to highest levels, are:
|
||||
|
||||
1. `silly` (default, meaning messages of all levels are logged)
|
||||
2. `debug`
|
||||
3. `info`
|
||||
4. `warn`
|
||||
5. `error`
|
||||
|
||||
You can change that by setting the `LOG_LEVEL` environment variable to the minimum level you want to be logged.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
LOG_LEVEL=error
|
||||
```
|
||||
|
||||
This logs `error` messages only.
|
||||
|
||||
<Note title="Important">
|
||||
|
||||
The environment variable must be set as a system environment variable and not in `.env`.
|
||||
|
||||
</Note>
|
||||
|
||||
### Save Logs in a File
|
||||
|
||||
Aside from showing the logs in the terminal, you can save the logs in a file by setting the `LOG_FILE` environment variable to the path of the file relative to the Medusa server’s root directory.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
LOG_FILE=all.log
|
||||
```
|
||||
|
||||
Your logs are now saved in the `all.log` file at the root of your Medusa application.
|
||||
|
||||
<Note title="Important">
|
||||
|
||||
The environment variable must be set as a system environment variable and not in `.env`.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Show Log with Progress
|
||||
|
||||
The `Logger` class has an `activity` method used to log a message of level `info`. If the Medusa application is running in a development environment, a spinner starts that can be used to show progress and succeed or fail the progress.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/loaders/log-message.ts"
|
||||
import { Logger, MedusaContainer } from "@medusajs/medusa"
|
||||
|
||||
export default async (container: MedusaContainer) => {
|
||||
const logger = container.resolve<Logger>("logger")
|
||||
|
||||
const activityId = logger.activity("First log message")
|
||||
|
||||
logger.progress(activityId, `Second log message`)
|
||||
|
||||
logger.success(activityId, "Last log message")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The `activity` method returns the ID of the started activity. This ID can then be passed to one of the following methods of the `Logger` class:
|
||||
|
||||
- `progress`: Log a message of level `info` that indicates progress within that same activity.
|
||||
- `success`: Log a message of level `info` that indicates that the activity has succeeded. This also ends the associated activity.
|
||||
- `failure`: Log a message of level `error` that indicates that the activity has failed. This also ends the associated activity.
|
||||
|
||||
<Note>
|
||||
|
||||
If you configured the `LOG_LEVEL` environment variable to a level higher than those associated with the above methods, their messages won’t be logged.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,13 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Debugging and Testing`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you’ll find some tips when debugging and testing your customizations in the Medusa application.
|
||||
|
||||
By the end of this chapter, you’ll learn:
|
||||
|
||||
- How to use Medusa’s `Logger` utility to log messages.
|
||||
- What tools to use for testing and debugging.
|
||||
- What feature flags are and how to enable them.
|
||||
@@ -0,0 +1,73 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Debugging and Testing Tools`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about debugging and testing tools you can use.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
Since the Medusa server is a Node.js server, you can use any Node.js testing tool you prefer, even if it’s not listed here.
|
||||
|
||||
</Note>
|
||||
|
||||
## Jest
|
||||
|
||||
[Jest](https://jestjs.io/) is a JavaScript testing framework. Your Medusa project is already configured with Jest; you can use it out-of-the-box.
|
||||
|
||||
For example, consider the following service created at `src/modules/hello/service.ts`:
|
||||
|
||||
```ts title="src/modules/hello/service.ts"
|
||||
import { TransactionBaseService } from "@medusajs/medusa"
|
||||
|
||||
class HelloWorldService extends TransactionBaseService {
|
||||
constructor(container) {
|
||||
super(arguments[0])
|
||||
}
|
||||
getMessage(): string {
|
||||
return "Hello, world!"
|
||||
}
|
||||
}
|
||||
|
||||
export default HelloWorldService
|
||||
|
||||
```
|
||||
|
||||
You can write a test for it in the file `src/modules/hello/__tests__/hello-world.ts`:
|
||||
|
||||
```ts title="src/modules/hello/__tests__/hello-world.ts"
|
||||
import HelloWorldService from "../hello-world"
|
||||
|
||||
describe("HelloWorldService", () => {
|
||||
const helloWorldService = new HelloWorldService()
|
||||
|
||||
it("should return hello world message", () => {
|
||||
expect(helloWorldService.getMessage()).toBe("Hello, world!")
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Then, run the following command in the root of your Medusa project to run the test:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run test
|
||||
```
|
||||
|
||||
This command looks for TypeScript and JavaScript files under any directory named `__tests__` and runs them.
|
||||
|
||||
---
|
||||
|
||||
## IDE Debugging Tools
|
||||
|
||||
Your IDE may provide a debugging tool for Node.js. In that case, you can use that tool to debug your Medusa customizations.
|
||||
|
||||
For example, if you’re using VSCode, refer to [this guide](https://code.visualstudio.com/docs/nodejs/nodejs-debugging) to learn how to configure and use the debugger.
|
||||
|
||||
---
|
||||
|
||||
## Node.js Debugger
|
||||
|
||||
You can also use Node.js’s Debugger API to debug your Medusa customizations.
|
||||
|
||||
Refer to [Node.js’s documentation](https://nodejs.org/docs/latest-v18.x/api/debugger.html) for more details.
|
||||
@@ -0,0 +1,95 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Deployment Overview`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about the different methods to deploy your Medusa application.
|
||||
|
||||
## Medusa Project Components
|
||||
|
||||
A standard Medusa project is made up of:
|
||||
|
||||
- Medusa application: The Medusa server and the Medusa Admin.
|
||||
- One or more storefronts
|
||||
|
||||

|
||||
|
||||
You can either deploy the Medusa application fully (server along with the admin), or deploy the Medusa Admin separately. As for the storefront, it's always deployed separately.
|
||||
|
||||
---
|
||||
|
||||
## Deploying the Medusa Server
|
||||
|
||||
You must deploy the Medusa server before the admin or storefront, as both of them connect to it and won’t work without a deployed Medusa server URL.
|
||||
|
||||

|
||||
|
||||
Since the Medusa server is a Node.js server, it must be deployed to a hosting provider that supports deploying servers, such as Railway, DigitalOcean, AWS, Heruko, etc…
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
For optimal experience, make sure that the hosting provider and plan offer at least 2GB of RAM.
|
||||
|
||||
</Note>
|
||||
|
||||
Your server connects to PostgreSQL and Redis databases. Most hosting providers support deploying and managing these databases along with your Medusa server (such as Railway and DigitalOcean). You can also choose a separate hosting for either of them and connect to them with Medusa’s configurations.
|
||||
|
||||
Refer to [this reference](!resources!/deployment) to find how-to deployment guides for specific hosting providers.
|
||||
|
||||
---
|
||||
|
||||
## Deploying the Medusa Admin
|
||||
|
||||
### Deploy Admin with the Server
|
||||
|
||||
Since the Medusa Admin is a plugin installed in the server, you may choose to host them together.
|
||||
|
||||
In this scenario, make sure the hosting provider and plan of your choice provide at least 2GB of RAM, as the admin build requires high RAM usage.
|
||||
|
||||

|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
The server deployment guides mention details on how to deploy the admin along with the backend.
|
||||
|
||||
</Note>
|
||||
|
||||
### Deploy Admin through GitHub Action
|
||||
|
||||
If you host your server's code on GitHub, you can:
|
||||
|
||||
1. Disable the `autoRebuild` option of the admin plugin.
|
||||
2. Create a GitHub action that builds the admin before the deployment is triggered.
|
||||
|
||||
With this solution, you can deploy the admin with the server but the hosting provider doesn’t handle the admin building, decreasing the RAM usage.
|
||||
|
||||

|
||||
|
||||
### Deploy Admin Separately
|
||||
|
||||
You can deploy the admin into a separate hosting provider or instance. The admin can be hosted on providers that support front-end websites and frameworks, such as Vercel.
|
||||
|
||||
<Note title="Important">
|
||||
|
||||
Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the Medusa Admin on the free plan for development purposes, but for commercial projects, you must update your Vercel plan.
|
||||
|
||||
</Note>
|
||||
|
||||
Refer to [this reference](!resources!/deployment) to find how-to deployment guides for specific hosting providers.
|
||||
|
||||
---
|
||||
|
||||
## Deploying the Storefront
|
||||
|
||||
The storefront is deployed separately from the Medusa application, and the hosting options depend on the tools and frameworks you use to create the storefront.
|
||||
|
||||
If you’re using the Next.js starter, you may deploy the storefront to any hosting provider that supports frontend frameworks, such as Vercel.
|
||||
|
||||
<Note title="Important">
|
||||
|
||||
Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the storefront on the free plan for development purposes, but for commercial projects, you must update your Vercel plan.
|
||||
|
||||
</Note>
|
||||
|
||||
Refer to [this reference](!resoureces!/deployment) to find how-to deployment guides for specific hosting providers.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,63 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Your First Customizations`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll build your first customizations with Medusa.
|
||||
|
||||
## 1. Create API Route
|
||||
|
||||
API Routes expose business logic through REST APIs. You can create custom API routes to expose custom business logic.
|
||||
|
||||
To create an API route, create the file `src/api/store/hello-world/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/store/hello-world/route.ts"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
export const GET = (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: "Hello, world!",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Test Customizations
|
||||
|
||||
To test out your customizations:
|
||||
|
||||
1. Start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
2. Send a `GET` request to `/store/hello-world`:
|
||||
|
||||
```bash apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/hello-world"
|
||||
curl http://localhost:9000/store/hello-world
|
||||
```
|
||||
|
||||
In the response, you’ll receive the following object:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Hello, world!"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
Congratulations, you’ve made your first customization with Medusa!
|
||||
|
||||
You can now start your in-depth learning journey to become a Medusa expert.
|
||||
@@ -0,0 +1,22 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply overflow-x-hidden;
|
||||
}
|
||||
|
||||
*::selection {
|
||||
@apply bg-medusa-bg-highlight;
|
||||
}
|
||||
|
||||
body[data-modal="opened"] {
|
||||
@apply !overflow-hidden;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Metadata } from "next"
|
||||
import { Inter, Roboto_Mono } from "next/font/google"
|
||||
|
||||
import Navbar from "@/components/Navbar"
|
||||
import Providers from "@/providers"
|
||||
import "./globals.css"
|
||||
import { TightLayout } from "docs-ui"
|
||||
import { config } from "@/config"
|
||||
import clsx from "clsx"
|
||||
import Feedback from "@/components/Feedback"
|
||||
import EditButton from "@/components/EditButton"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: `%s - ${config.titleSuffix}`,
|
||||
default: config.titleSuffix || "",
|
||||
},
|
||||
description: "Explore and learn how to use Medusa.",
|
||||
metadataBase: new URL(
|
||||
process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"
|
||||
),
|
||||
}
|
||||
|
||||
export const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
weight: ["400", "500"],
|
||||
})
|
||||
|
||||
export const robotoMono = Roboto_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-roboto-mono",
|
||||
})
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<TightLayout
|
||||
ProvidersComponent={Providers}
|
||||
NavbarComponent={Navbar}
|
||||
sidebarProps={{
|
||||
expandItems: true,
|
||||
}}
|
||||
showPagination={true}
|
||||
bodyClassName={clsx(inter.variable, robotoMono.variable)}
|
||||
>
|
||||
{children}
|
||||
<Feedback className="my-2" />
|
||||
<EditButton />
|
||||
</TightLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { CheckCircleSolid, BuildingStorefront, BuildingsSolid, ComputerDesktop } from "@medusajs/icons"
|
||||
import { config } from "@/config"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Introduction - ${config.titleSuffix}`,
|
||||
}
|
||||
|
||||
# {pageNumber} Introduction
|
||||
|
||||
Medusa is a digital commerce platform with a built-in framework for customizations and integrations.
|
||||
|
||||
Medusa's framework includes two types of tools:
|
||||
|
||||
1. Modules that provide commerce functionalities, such as Product, Order, and Cart modules.
|
||||
2. Customization tools to build your business features, such as custom data models, business logic, flows, and API endpoints.
|
||||
|
||||
With these tools, you save time you would spend with other platforms on maintaining and setting up your customizations. Instead, you rapidly build important features for your business.
|
||||
|
||||
---
|
||||
|
||||
## Get started
|
||||
|
||||
|
||||
<Note type="check">
|
||||
|
||||
- [Node.js v16+](https://nodejs.org/en/download)
|
||||
- [Git CLI tool](https://git-scm.com/downloads)
|
||||
- [PostgreSQL](https://www.postgresql.org/download/)
|
||||
|
||||
</Note>
|
||||
|
||||
Create your first Medusa store by running the following command:
|
||||
|
||||
```bash
|
||||
npx create-medusa-app@latest
|
||||
```
|
||||
|
||||
This takes you on a journey to set up your store, learn about Medusa, and build your first customizations.
|
||||
|
||||
---
|
||||
|
||||
## Who is Medusa for
|
||||
|
||||
Medusa is for ambitious businesses and developers that are limited by traditional ecommerce solutions:
|
||||
|
||||
- You want rich commerce features that are customizable and flexible enough to support your business use case as if you built them from scratch.
|
||||
- You want your custom requirements built as an integral part of the commerce application’s features and architecture, rather than hacked into it.
|
||||
- You want to customize your commerce flows by integrating external services without data inconsistency and irregularity.
|
||||
|
||||
<CardList items={[
|
||||
{
|
||||
title: "Use Case: D2C",
|
||||
text: "How Matt Sleeps built a unique D2C experience with Medusa",
|
||||
href: "https://medusajs.com/blog/matt-sleeps/",
|
||||
startIcon: <CheckCircleSolid />
|
||||
},
|
||||
{
|
||||
title: "Use Case: OMS",
|
||||
text: "How Makro Pro Built an OMS with Medusa",
|
||||
href: "https://medusajs.com/blog/makro-pro/",
|
||||
startIcon: <CheckCircleSolid />
|
||||
},
|
||||
{
|
||||
title: "Use Case: Marketplace",
|
||||
text: "How Foraged built a custom marketplace with Medusa",
|
||||
href: "https://medusajs.com/blog/foraged/",
|
||||
startIcon: <BuildingStorefront />
|
||||
},
|
||||
{
|
||||
title: "Use Case: POS",
|
||||
text: "How Tekla built a global webshop and a POS system with Medusa",
|
||||
href: "https://medusajs.com/blog/tekla-pos/",
|
||||
startIcon: <ComputerDesktop />
|
||||
},
|
||||
{
|
||||
title: "Use Case: B2B",
|
||||
text: "How Visionary built B2B commerce with Medusa",
|
||||
href: "https://medusajs.com/blog/visionary/",
|
||||
startIcon: <BuildingsSolid />
|
||||
},
|
||||
{
|
||||
title: "Use Case: Platform",
|
||||
text: "How Catalog built a B2B platform for SMBs with Medusa",
|
||||
href: "https://medusajs.com/blog/catalog/",
|
||||
startIcon: <BuildingsSolid />
|
||||
}
|
||||
]} itemsPerRow={2} />
|
||||
|
||||
---
|
||||
|
||||
## Who is this book for
|
||||
|
||||
This book is for TypeScript or JavaScript developers looking to master Medusa and build their commerce applications. By following this book, you’ll learn about Medusa’s concept, from basic to advanced, with easy-to-follow examples to assist you along the way.
|
||||
|
||||
By the end of this book, you’ll be a Medusa expert, leading teams using Medusa from the beginning till deployment.
|
||||
@@ -0,0 +1,67 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Next.js Starter`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to install and use the Next.js Starter storefront.
|
||||
|
||||
<Note title="Demo">
|
||||
|
||||
Check out the demo [here](https://next.medusajs.com/).
|
||||
|
||||
</Note>
|
||||
|
||||
## Install Next.js Starter
|
||||
|
||||
<Note type="check">
|
||||
|
||||
- [Node v18+](https://nodejs.org/en/)
|
||||
- At least one region in the Medusa application.
|
||||
|
||||
</Note>
|
||||
|
||||
1. Create a Next.js project using the [Next.js Starter’s repository URL](https://github.com/medusajs/nextjs-starter-medusa):
|
||||
|
||||
```bash
|
||||
npx create-next-app -e https://github.com/medusajs/nextjs-starter-medusa my-medusa-storefront
|
||||
```
|
||||
|
||||
2. Change to the `my-medusa-storefront` directory and rename the template environment variable:
|
||||
|
||||
```bash
|
||||
cd my-medusa-storefront
|
||||
mv .env.template .env.local
|
||||
```
|
||||
|
||||
3. While the Medusa application is running, start the Next.js storefront:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Your Next.js Starter Storefront is now running at `localhost:8000`.
|
||||
|
||||
---
|
||||
|
||||
## Customize Storefront
|
||||
|
||||
To customize the storefront, refer to the following directories:
|
||||
|
||||
- `src/app`: The storefront’s pages.
|
||||
- `src/modules`: The storefront’s components.
|
||||
- `src/styles`: The storefront’s styles.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
You can learn more about development with Next.js through [their documentation](https://nextjs.org/docs/getting-started).
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Configurations and Compatible Plugins
|
||||
|
||||
The Next.js Starter is compatible with some Medusa plugins out-of-the-box, such as the MeiliSearch or Stripe plugins. You can also change some of its configurations if necessary.
|
||||
|
||||
Refer to the Next.js Starter reference for more details.
|
||||
@@ -0,0 +1,13 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Storefront Development`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you’ll learn about building a storefront for your Medusa backend.
|
||||
|
||||
## Storefronts in Medusa’s Architecture
|
||||
|
||||
Storefronts are built separately from the Medusa application. They interact with the Medusa application through the Store API Routes.
|
||||
|
||||
You have full freedom in how you choose to build your storefront. You can start with our Next.js starter storefront or build a storefront from scratch using your preferred framework or tech stack. Both of these approaches are explained in the next chapters.
|
||||
@@ -0,0 +1,67 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Storefront Development Tips`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll find tips to help you build a storefront.
|
||||
|
||||
## Connect to the Medusa Application
|
||||
|
||||
<Note type="soon">
|
||||
|
||||
Support for Medusa v2 in Medusa React and the JS Client is coming soon.
|
||||
|
||||
</Note>
|
||||
|
||||
To send requests from the storefront to the Medusa application’s Store API Routes, you have three options:
|
||||
|
||||
- **For React-based storefronts**: use Medusa React. It provides you with the necessary hooks to retrieve or manipulate data from your Medusa application.
|
||||
- **For JavaScript frameworks**: use Medusa’s JavaScript Client in any JavaScript framework. This NPM package facilitates interacting with the backend’s REST APIs.
|
||||
- **For other frontend technologies**: interact directly with the Medusa application by sending requests to its [Store REST APIs](https://docs.medusajs.com/api/store).
|
||||
|
||||
---
|
||||
|
||||
## Configure CORS
|
||||
|
||||
The Medusa application’s API routes are guarded by a CORS middleware. Make sure to set the `store_cors` configuration of your Medusa application to the storefront’s URL.
|
||||
|
||||
For example:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
module.exports = {
|
||||
projectConfig: {
|
||||
store_cors: "http://localhost:3000",
|
||||
// ...
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use Publishable API Keys
|
||||
|
||||
Publishable API keys specify the scope of a request. Admin users create them, and they can only be used on the client-side, such as in a storefront.
|
||||
|
||||
Publishable API keys are associated with sales channels. Then, when the publishable API key is passed in the header of a request, the Medusa application automatically infers what sales channel is being used.
|
||||
|
||||
<Note>
|
||||
|
||||
Products, carts, and orders are associated with a sales channel. If you don’t specify a sales channel or a publishable API key, the products, carts, and orders are associated with the default sales channel.
|
||||
|
||||
</Note>
|
||||
|
||||
### Create a Publishable API Key
|
||||
|
||||
You can create a publishable API key using either the [Medusa Admin dashboard](https://docs.medusajs.com/user-guide/settings/publishable-api-keys) or the [Admin API Routes](https://docs.medusajs.com/api/admin#publishable-api-keys).
|
||||
|
||||
### Use the Publishable API Key
|
||||
|
||||
Refer to the [Store API Reference](https://docs.medusajs.com/api/store#publishable-api-key) to learn how to use the publishable API key in your requests.
|
||||
|
||||
---
|
||||
|
||||
## Implement Commerce Functionalities
|
||||
|
||||
Refer to the [Commerce Modules reference](!resources!/commerce-modules) for how-to guides on common commerce functionalities.
|
||||
Reference in new issue
Block a user