docs: general fixes and improvements (#7918)

* docs improvements and changes

* updated module definition

* modules + dml changes

* fix build

* fix vale error

* fix lint errors

* fixes to stripe docs

* fix condition

* fix condition

* fix module defintion

* fix checkout

* disable UI action

* change oas preview action

* flatten provider module options

* fix lint errors

* add module link docs

* pr comments fixes

* fix vale error

* change node engine version

* links -> linkable

* add note about database name

* small fixes

* link fixes

* fix response code in api reference

* added migrations step
This commit is contained in:
Shahed Nasser authored and GitHub committed 2024-07-04 17:26:03 +03:00
1 parent 32982e708a
commit 964927b597
116 files changed
+948 -2280

No files matched your search

@@ -6,5 +6,60 @@ on:
jobs: jobs:
preview-references: preview-references:
uses: ./.github/workflows/generate-resources-reference.yml uses: ./.github/workflows/generate-resources-reference.yml
preview-api-ui: preview-api:
uses: ./.github/workflows/generate-public-references.yml name: Generate OAS
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node.js environment
uses: actions/setup-node@v3
with:
node-version: 20
cache: "yarn"
- name: Install dependencies
uses: ./.github/actions/cache-deps
with:
extension: reference
- name: Build Packages
run: yarn build
- name: Install www/utils Dependencies
run: yarn
working-directory: www/utils
- name: Build www/utils packages
run: yarn build
working-directory: www/utils
- name: Run docblock generator
run: "yarn generate:oas"
working-directory: www/utils
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_OWNER: ${{ github.repository_owner }}
GIT_REPO: medusa
- name: Generate API Reference (v2)
run: yarn openapi:generate
- name: Create Pull Request
uses: peter-evans/create-pull-request@v4
with:
commit-message: "chore(docs): Generated API Reference (v2)"
base: "develop"
title: "chore(docs): Updated API Reference (v2)"
labels: "type: chore"
add-paths: www/apps/api-reference/specs
branch: "docs/generate-api-ref"
branch-suffix: "timestamp"
@@ -1,6 +1,5 @@
name: Generate Public References name: Generate Public References
on: on:
workflow_call:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
referenceName: referenceName:
@@ -13,7 +12,7 @@ on:
jobs: jobs:
api-v2: api-v2:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event.inputs.referenceName == 'all' || github.event.inputs.referenceName == 'api' }} if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' || github.event.inputs.referenceName == 'all' || github.event.inputs.referenceName == 'api' }}
steps: steps:
- name: Cancel Previous Runs - name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0 uses: styfle/cancel-workflow-action@0.11.0
@@ -23,7 +22,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
token: ${{ secrets.REFERENCE_PAT }}
fetch-depth: 0 fetch-depth: 0
- name: Setup Node.js environment - name: Setup Node.js environment
@@ -63,7 +61,7 @@ jobs:
branch-suffix: "timestamp" branch-suffix: "timestamp"
ui: ui:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event.inputs.referenceName == 'all' || github.event.inputs.referenceName == 'ui' }} if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' || github.event.inputs.referenceName == 'all' || github.event.inputs.referenceName == 'ui' }}
steps: steps:
- name: Cancel Previous Runs - name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0 uses: styfle/cancel-workflow-action@0.11.0
@@ -73,7 +71,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
token: ${{ secrets.REFERENCE_PAT }}
fetch-depth: 0 fetch-depth: 0
- name: Setup Node.js environment - name: Setup Node.js environment
@@ -60,12 +60,12 @@ jobs:
working-directory: www/utils working-directory: www/utils
- name: Generate References - name: Generate References
if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' }} if: ${{ github.event_name == 'workflow_dispatch' }}
run: "yarn start generate ${{ github.event.inputs.referenceName }} --merge" run: "yarn start generate ${{ github.event.inputs.referenceName || 'all' }} --merge"
working-directory: www/utils/packages/typedoc-generate-references working-directory: www/utils/packages/typedoc-generate-references
- name: Generate References - name: Generate References
if: ${{ github.event_name != 'workflow_dispatch' || github.event_name == 'schedule' }} if: ${{ github.event_name != 'workflow_dispatch' }}
run: "yarn generate:references" run: "yarn generate:references"
working-directory: www/utils working-directory: www/utils
@@ -18,11 +18,12 @@ const TagsOperationDescriptionSectionResponses = ({
className={clsx("[&>details:not(:first-of-type)>summary]:border-t-0")} className={clsx("[&>details:not(:first-of-type)>summary]:border-t-0")}
> >
{Object.entries(responses).map(([code, response], index) => { {Object.entries(responses).map(([code, response], index) => {
const successCode = code.startsWith("20")
return ( return (
<Fragment key={index}> <Fragment key={index}>
{response.content && ( {response.content && (
<> <>
{(code === "200" || code === "201") && ( {successCode && (
<> <>
<DetailsSummary <DetailsSummary
title={`${code} ${response.description}`} title={`${code} ${response.description}`}
@@ -43,7 +44,7 @@ const TagsOperationDescriptionSectionResponses = ({
/> />
</> </>
)} )}
{code !== "200" && code !== "201" && ( {!successCode && (
<Details <Details
summaryElm={ summaryElm={
<DetailsSummary <DetailsSummary
@@ -72,12 +73,8 @@ const TagsOperationDescriptionSectionResponses = ({
title={`${code} ${response.description}`} title={`${code} ${response.description}`}
subtitle={"Empty response"} subtitle={"Empty response"}
badge={ badge={
<Badge <Badge variant={successCode ? "green" : "red"}>
variant={ {successCode ? "Success" : "Error"}
code === "200" || code === "201" ? "green" : "red"
}
>
{code === "200" || code === "201" ? "Success" : "Error"}
</Badge> </Badge>
} }
expandable={false} expandable={false}
+1 -1
View File
@@ -59,6 +59,6 @@
"types": "*" "types": "*"
}, },
"engines": { "engines": {
"node": ">=18.17.0" "node": ">=20"
} }
} }
@@ -1,85 +0,0 @@
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.
![Diagram visualizing the plugin development process.](https://res.cloudinary.com/dza7lstvk/image/upload/v1708506149/Medusa%20Book/plugin-development-process_hls7bh.jpg)
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.
@@ -1,102 +0,0 @@
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.
-127
View File
@@ -1,127 +0,0 @@
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,
},
},
]
```
@@ -1,144 +0,0 @@
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}`)
}
```
@@ -1,184 +0,0 @@
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.
@@ -1,82 +0,0 @@
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.
@@ -1,56 +0,0 @@
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.
![A diagram illustrating the created stage](https://res.cloudinary.com/dza7lstvk/image/upload/v1708688547/Medusa%20Book/batch-job-created_gthkwh.jpg)
---
## 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.
![A diagram illustrating the pre-processed stage](https://res.cloudinary.com/dza7lstvk/image/upload/v1708524703/Medusa%20Book/batch-job-pre_processed_jx0tvt.jpg)
---
## 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>
![A diagram illustrating the confirmed stage](https://res.cloudinary.com/dza7lstvk/image/upload/v1708525219/Medusa%20Book/batch-job-confirmed_e9pe05.jpg)
---
## 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.
![A diagram illustrating the processing and completed stages](https://res.cloudinary.com/dza7lstvk/image/upload/v1708526149/Medusa%20Book/batch-job-processing-completed_svniou.jpg)
---
## 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.
@@ -1,46 +0,0 @@
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>
@@ -1,95 +0,0 @@
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.
@@ -1,216 +0,0 @@
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>
@@ -1,111 +0,0 @@
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]
})
}
}
```
@@ -1,71 +0,0 @@
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.
@@ -8,11 +8,7 @@ In this chapter, you'll find some tips for your admin development.
## Routing Functionalities ## 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: To navigate or link to other pages, or perform other routing functionalities, use the [react-router-dom](https://reactrouter.com/en/main) package. It's installed in your project through the Medusa Admin.
```bash npm2yarn
npm install react-router-dom
```
For example: For example:
@@ -37,7 +33,7 @@ const ProductWidget = () => {
// The widget's configurations // The widget's configurations
export const config = defineWidgetConfig({ export const config = defineWidgetConfig({
zone: "product.details.after", zone: "product.details.before",
}) })
export default ProductWidget export default ProductWidget
@@ -14,15 +14,17 @@ For example, you may add a new page to manage product reviews.
--- ---
## How to Create a UI Route ## 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. 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: For example, create the file `src/admin/routes/custom/page.tsx` with the following content:
```tsx title="src/admin/routes/custom/page.tsx" ```tsx title="src/admin/routes/custom/page.tsx"
import { Container } from "@medusajs/ui"
const CustomPage = () => { const CustomPage = () => {
return <div>This is my custom route</div> return <Container>This is my custom route</Container>
} }
export default CustomPage export default CustomPage
@@ -30,7 +32,9 @@ export default CustomPage
The new page’s path is the file’s path relative to `src/admin/routes`. So, the above UI route is a new page added at the path `localhost:9000/app/custom`. The new page’s path is the file’s path relative to `src/admin/routes`. So, the above UI route is a new page added at the path `localhost:9000/app/custom`.
### Test the UI Route ---
## Test the UI Route
To test the UI route, start the Medusa application: To test the UI route, start the Medusa application:
@@ -56,37 +60,6 @@ export const highlights = [
```tsx title="src/admin/routes/custom/page.tsx" highlights={[["21"], ["22"], ["23"], ["24"], ["25"], ["26"]]} ```tsx title="src/admin/routes/custom/page.tsx" highlights={[["21"], ["22"], ["23"], ["24"], ["25"], ["26"]]}
import { defineRouteConfig } from "@medusajs/admin-shared" import { defineRouteConfig } from "@medusajs/admin-shared"
import { ChatBubbleLeftRight } from "@medusajs/icons" import { ChatBubbleLeftRight } from "@medusajs/icons"
const CustomPage = () => {
return <div>This is my custom route</div>
}
export const config = defineRouteConfig({
label: "Custom Route",
icon: ChatBubbleLeftRight,
})
export default CustomPage
```
The configuration object is creaetd by the `defineRouteConfig` function imported from `@medusajs/admin-shared`. It accepts the following properties:
- `label`: the new sidebar item’s label.
- `icon`: an optional React component that acts as an icon in the sidebar.
The above example adds a new sidebar item with the label `Custom Route` and an icon from the [Medusa UI Icons package](!ui!/icons/overview).
---
## 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 { defineRouteConfig } from "@medusajs/admin-shared"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container } from "@medusajs/ui" import { Container } from "@medusajs/ui"
const CustomPage = () => { const CustomPage = () => {
@@ -101,6 +74,13 @@ export const config = defineRouteConfig({
export default CustomPage export default CustomPage
``` ```
The configuration object is created using the `defineRouteConfig` function imported from `@medusajs/admin-shared`. It accepts the following properties:
- `label`: the sidebar item’s label.
- `icon`: an optional React component used as an icon in the sidebar.
The above example adds a new sidebar item with the label `Custom Route` and an icon from the [Medusa UI Icons package](!ui!/icons/overview).
--- ---
## Create Settings Page ## Create Settings Page
@@ -134,13 +114,7 @@ This adds a page under the path `/app/settings/custom`. An item is also added to
## Path Parameters ## 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`. A UI route can accept path parameters if the name of any of the directories in its path is of the format `[param]`.
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: For example, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content:
@@ -157,4 +131,6 @@ const CustomPage = () => {
export default CustomPage export default CustomPage
``` ```
You access the passed parameter using `react-router-dom`'s [useParams hook](https://reactrouter.com/en/main/hooks/use-params).
If you run the Medusa application and go to `localhost:9000/app/custom/123`, you'll see `123` printed in the page. If you run the Medusa application and go to `localhost:9000/app/custom/123`, you'll see `123` printed in the page.
@@ -23,96 +23,15 @@ A widget is created in a file under the `src/admin/widgets` directory. The file
For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: For example, create the file `src/admin/widgets/product-widget.tsx` with the following content:
export const widgetHighlights = [ export const widgetHighlights = [
["4", "ProductWidget", "The React component of the product widget."], ["5", "ProductWidget", "The React component of the product widget."],
["14", "zone", "The zone to inject the widget to."] ["15", "zone", "The zone to inject the widget to."]
] ]
```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} ```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights}
import { defineWidgetConfig } from "@medusajs/admin-shared" import { defineWidgetConfig } from "@medusajs/admin-shared"
// The widget
const ProductWidget = () => {
return (
<div>
<h2>Product Widget</h2>
</div>
)
}
// The widget's configurations
export const config = defineWidgetConfig({
zone: "product.details.after",
})
export default ProductWidget
```
The widget only shows the heading `Product Widget`.
Use the `defineWidgetConfig` function imported from `@medusajs/admin-shared` to create and export the widget's configurations.
The function accepts as a parameter an object with the following property:
- `zone`: A string or an array of strings, each being the name of the zone to inject the widget into.
In the example above, the widget is injected 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.
---
## Detail Widget Props
Widgets that are injected into a details page (for example, `product.details.after`) receive a `data` prop, which is the main data of the details page (for example, the product object).
For example:
```tsx title="src/admin/widgets/product-widget.tsx" highlights={[["8"]]}
import { defineWidgetConfig } from "@medusajs/admin-shared"
import {
DetailWidgetProps,
AdminProduct,
} from "@medusajs/types"
const ProductWidget = ({
data,
}: DetailWidgetProps<AdminProduct>) => {
return (
<div>
<h2>Product Widget {data.title}</h2>
</div>
)
}
export const config = defineWidgetConfig({
zone: "product.details.after",
})
export default ProductWidget
```
Notice that the type of the props is `DetailWidgetProps`, which accepts as a type argument the expected type of the data.
---
## 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 { defineWidgetConfig } from "@medusajs/admin-shared"
import { Container, Heading } from "@medusajs/ui" import { Container, Heading } from "@medusajs/ui"
// The widget
const ProductWidget = () => { const ProductWidget = () => {
return ( return (
<Container> <Container>
@@ -121,18 +40,78 @@ const ProductWidget = () => {
) )
} }
export const config: WidgetConfig = defineWidgetConfig({ // The widget's configurations
zone: "product.details.after", export const config = defineWidgetConfig({
zone: "product.details.before",
}) })
export default ProductWidget export default ProductWidget
``` ```
<Note title="Tip"> The widget only shows the heading `Product Widget`.
Admin Widgets also support [Tailwind CSS](https://tailwindcss.com/) out of the box. Use the `defineWidgetConfig` function imported from `@medusajs/admin-shared` to create and export the widget's configurations. It accepts as a parameter an object with the following property:
</Note> - `zone`: A string or an array of strings, each being the name of the zone to inject the widget into.
In the example above, the widget is injected at the top of a product’s details.
---
## Test the Widget
To test out the widget, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, open a product’s details page. You’ll find your custom widget at the top of the page.
---
## Detail Widget Props
Widgets that are injected into a details page (for example, `product.details.before`) receive a `data` prop, which is the main data of the details page (for example, the product object).
For example:
export const detailHighlights = [
["10", "data", "Receive the data as a prop."],
["11", "AdminProduct", "Pass the expected type of `data` as a type argument."],
["15", "data.title"]
]
```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights}
import { defineWidgetConfig } from "@medusajs/admin-shared"
import { Container, Heading } from "@medusajs/ui"
import {
DetailWidgetProps,
AdminProduct,
} from "@medusajs/types"
// The widget
const ProductWidget = ({
data,
}: DetailWidgetProps<AdminProduct>) => {
return (
<Container>
<Heading level="h2">
Product Widget {data.title}
</Heading>
</Container>
)
}
// The widget's configurations
export const config = defineWidgetConfig({
zone: "product.details.before",
})
export default ProductWidget
```
Notice that the type of the props is `DetailWidgetProps`, which accepts as a type argument the expected type of `data`.
--- ---
@@ -8,7 +8,9 @@ In this chapter, you’ll learn about the CORS middleware and how to configure i
## CORS Overview ## 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. 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 ### CORS Configurations
@@ -30,6 +32,12 @@ module.exports = defineConfig({
This allows the `http://localhost:7001` origin to access the Admin API Routes, and the `http://localhost:8000` origin to access Store API Routes. This allows the `http://localhost:7001` origin to access the Admin API Routes, and the `http://localhost:8000` origin to access Store API Routes.
<Note title="Tip">
Learn more about the CORS configurations in [this resource guide](!resources!/references/medusa-config#http).
</Note>
--- ---
## CORS in Store and Admin Routes ## CORS in Store and Admin Routes
@@ -70,13 +78,16 @@ You can do that in the exported middlewares configurations in `src/api/middlewar
For example: For example:
export const highlights = [["18", "parseCorsOrigins", "A utility function that parses the CORS configurations in `medusa-config.js`"]] export const highlights = [["25", "parseCorsOrigins", "A utility function that parses the CORS configurations in `medusa-config.js`"]]
```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" ```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
import { import {
ConfigModule,
MiddlewaresConfig, MiddlewaresConfig,
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import { ConfigModule } from "@medusajs/types"
import { parseCorsOrigins } from "@medusajs/utils" import { parseCorsOrigins } from "@medusajs/utils"
import cors from "cors" import cors from "cors"
@@ -85,7 +96,11 @@ export const config: MiddlewaresConfig = {
{ {
matcher: "/custom*", matcher: "/custom*",
middlewares: [ middlewares: [
(req, res, next) => { (
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => {
const configModule: ConfigModule = const configModule: ConfigModule =
req.scope.resolve("configModule") req.scope.resolve("configModule")
@@ -6,9 +6,9 @@ export const metadata = {
In this chapter, you'll learn about how to add new API routes for each HTTP method. In this chapter, you'll learn about how to add new API routes for each HTTP method.
## Handlers of HTTP Methods ## HTTP Method Handler
You can export handler functions for more than one HTTP method in a route file. An API route is created for every HTTP method you export a function for. An API route is created for every HTTP method you export a handler function for in a route file.
Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`. Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`.
@@ -41,5 +41,5 @@ export const POST = (
This adds two API Routes: This adds two API Routes:
- A `GET` route at `localhost:9000/store/hello-world`. - A `GET` route at `http://localhost:9000/store/hello-world`.
- A `POST` route at `localhost:9000/store/hello-world`. - A `POST` route at `http://localhost:9000/store/hello-world`.
@@ -8,7 +8,7 @@ In this chapter, you’ll learn about middlewares and how to create them.
## What is a Middleware? ## What is a Middleware?
A middleware is a function executed when a request is sent to an API Route. A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler function.
--- ---
@@ -19,14 +19,23 @@ Middlewares are defined in the special file `src/api/middlewares.ts`. The file m
For example: For example:
```ts title="src/api/middlewares.ts" ```ts title="src/api/middlewares.ts"
import { MiddlewaresConfig } from "@medusajs/medusa" import type {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
MiddlewaresConfig,
} from "@medusajs/medusa"
export const config: MiddlewaresConfig = { export const config: MiddlewaresConfig = {
routes: [ routes: [
{ {
matcher: "/store*", matcher: "/store*",
middlewares: [ middlewares: [
(req, res, next) => { (
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => {
console.log("Received a request!") console.log("Received a request!")
next() next()
@@ -37,17 +46,16 @@ export const config: MiddlewaresConfig = {
} }
``` ```
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. The middleware configurations object has the property `routes`. Its value is an array of middleware route objects, each having the following properties:
- `matcher`: a string or regular expression indicating the API route path to apply the middleware on.
- `middlewares`: An array of middleware functions.
In the example above, you define a middleware that logs the message `Received a request!` whenever a request is sent to an API route path starting with `/store`. 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. ## Test the Middleware
</Note>
### Test Middleware
To test the middleware: To test the middleware:
@@ -99,10 +107,15 @@ In addition to the `matcher` configuration, you can restrict which HTTP methods
For example: For example:
export const highlights = [["7", "", "Apply the middleware only on `POST` requests"]] export const highlights = [["12", "method", "Apply the middleware only on `POST` requests"]]
```ts title="src/api/middlewares.ts" highlights={highlights} ```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { MiddlewaresConfig } from "@medusajs/medusa" import type {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
MiddlewaresConfig,
} from "@medusajs/medusa"
export const config: MiddlewaresConfig = { export const config: MiddlewaresConfig = {
routes: [ routes: [
@@ -110,7 +123,11 @@ export const config: MiddlewaresConfig = {
matcher: "/store*", matcher: "/store*",
method: ["POST", "PUT"], method: ["POST", "PUT"],
middlewares: [ middlewares: [
(req, res, next) => { (
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => {
console.log("Received a request!") console.log("Received a request!")
next() next()
@@ -8,7 +8,7 @@ In this chapter, you’ll learn about path, query, and request body parameters.
## Path Parameters ## Path Parameters
To create an API route that accepts a path parameter, create a directory within the route's path whose name is of the format `[param]`. To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`.
For example, to create an API Route at the path `/message/{id}`, where `{id}` is a path parameter, create the file `src/api/store/hello-world/[id]/route.ts` with the following content: For example, to create an API Route at the path `/message/{id}`, where `{id}` is a path parameter, create the file `src/api/store/hello-world/[id]/route.ts` with the following content:
@@ -16,7 +16,7 @@ export const singlePathHighlights = [
["11", "req.params.id", "Access the path parameter `id`"] ["11", "req.params.id", "Access the path parameter `id`"]
] ]
```ts title="src/api/store/hello-world/[id]/route.ts" highlights={singlePathHighlights} ```ts title="src/api/store/hello-world/[id]/route.ts" highlights={singlePathHighlights} apiTesting testApiUrl="http://localhost:9000/store/hello-world/{id}" testApiMethod="GET" testPathParams={{ "id": "1" }}
import type { import type {
MedusaRequest, MedusaRequest,
MedusaResponse, MedusaResponse,
@@ -45,7 +45,7 @@ export const multiplePathHighlights = [
["13", "req.params.name", "Access the path parameter `name`"] ["13", "req.params.name", "Access the path parameter `name`"]
] ]
```ts title="src/api/store/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} ```ts title="src/api/store/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} apiTesting testApiUrl="http://localhost:9000/store/hello-world/{id}/name/{name}" testApiMethod="GET" testPathParams={{ "id": "1", "name": "John" }}
import type { import type {
MedusaRequest, MedusaRequest,
MedusaResponse, MedusaResponse,
@@ -77,7 +77,7 @@ export const queryHighlights = [
["11", "req.query.name", "Access the query parameter `name`"], ["11", "req.query.name", "Access the query parameter `name`"],
] ]
```ts title="src/api/store/hello-world/route.ts" highlights={queryHighlights} ```ts title="src/api/store/hello-world/route.ts" highlights={queryHighlights} apiTesting testApiUrl="http://localhost:9000/store/hello-world" testApiMethod="GET" testQueryParams={{ "name": "John" }}
import type { import type {
MedusaRequest, MedusaRequest,
MedusaResponse, MedusaResponse,
@@ -6,10 +6,14 @@ export const metadata = {
In this chapter, you’ll learn how to create protected routes. In this chapter, you’ll learn how to create protected routes.
## Default Protected Routes ## What is a Protected Route?
A protected route is a route that requires requests to be user-authenticated before performing the route's functionality. Otherwise, the request fails, and the user is prevented access. A protected route is a route that requires requests to be user-authenticated before performing the route's functionality. Otherwise, the request fails, and the user is prevented access.
---
## Default Protected Routes
Medusa applies an authentication guard on the following routes: Medusa applies an authentication guard on the following routes:
- Routes starting with `/admin` require an authenticated admin user. - Routes starting with `/admin` require an authenticated admin user.
@@ -41,13 +45,13 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
export const AUTHENTICATE = false export const AUTHENTICATE = false
``` ```
Now, any request sent to the `/store/customers/me/custom` API route is allowed, regardless if the customer is authenticated or not. Now, any request sent to the `/store/customers/me/custom` API route is allowed, regardless if the customer is authenticated.
--- ---
## Access Logged-In Customer ## 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. You can access the logged-in customer’s ID in all API routes starting with `/store` using the `auth_context.actor_id` property of the `MedusaRequest` object.
For example: For example:
@@ -67,7 +71,7 @@ export const GET = async (
ModuleRegistrationName.CUSTOMER ModuleRegistrationName.CUSTOMER
) )
const customer = await customerModuleService.retrieve( const customer = await customerModuleService.retrieveCustomer(
req.auth_context.actor_id req.auth_context.actor_id
) )
@@ -75,13 +79,13 @@ export const GET = async (
} }
``` ```
In the route handler, you resolve the Customer Module's main service, then use it to retrieve the logged-in customer, if available. In this example, you resolve the Customer Module's main service, then use it to retrieve the logged-in customer, if available.
--- ---
## Access Logged-In Admin User ## 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. You can access the logged-in admin user’s ID in all API Routes starting with `/admin` using the `auth_context.actor_id` property of the `MedusaRequest` object.
For example: For example:
@@ -97,17 +101,19 @@ export const GET = async (
req: AuthenticatedMedusaRequest, req: AuthenticatedMedusaRequest,
res: MedusaResponse res: MedusaResponse
) => { ) => {
const userService: IUserModuleService = req.scope.resolve( const userModuleService: IUserModuleService = req.scope.resolve(
ModuleRegistrationName.USER ModuleRegistrationName.USER
) )
const user = await userService.retrieve(req.auth_context.actor_id) const user = await userModuleService.retrieveUser(
req.auth_context.actor_id
)
// ... // ...
} }
``` ```
In the route handler, you resolve the User Module's main service, and then use it to retrieve the logged-in admin user. In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user.
--- ---
@@ -151,6 +157,5 @@ The `authenticate` middleware function accepts three parameters:
1. The type of user authenticating. Use `user` for authenticating admin users, and `customer` for authenticating customers. 1. The type of user authenticating. Use `user` for authenticating admin users, and `customer` for authenticating customers.
2. An array of the types of authentication methods allowed. Both `user` and `customer` scopes support `session` and `bearer`. The `admin` scope also supports the `api-key` authentication method. 2. An array of the types of authentication methods allowed. Both `user` and `customer` scopes support `session` and `bearer`. The `admin` scope also supports the `api-key` authentication method.
3. An optional object of options having the following properties: 3. An optional object of configurations accepting the following property:
1. `allowUnauthenticated`: (default: `false`) A boolean indicating whether authentication is required. For example, you may have an API route where you want to access the logged-in customer if available, but guest customers can still access it too. In that case, enable the `allowUnauthenticated` option. - `allowUnauthenticated`: (default: `false`) A boolean indicating whether authentication is required. For example, you may have an API route where you want to access the logged-in customer if available, but guest customers can still access it too.
2. `allowUnregistered`: (default: `false`) A boolean indicating whether new users can be authenticated.
@@ -8,7 +8,7 @@ In this chapter, you'll learn how create and execute custom scripts from Medusa'
## What is a Custom CLI Script? ## What is a Custom CLI Script?
A custom CLI script is a function to execute through Medusa's CLI tool. This is useful when creating custom Medusa tooling to run as a CLI tool. A custom CLI script is a function to execute through Medusa's CLI tool. This is useful when creating custom Medusa tooling to run through the CLI.
--- ---
@@ -19,7 +19,10 @@ To create a custom CLI script, create a TypeScript or JavaScript file under the
For example, create the file `src/scripts/my-script.ts` with the following content: For example, create the file `src/scripts/my-script.ts` with the following content:
```ts title="src/scripts/my-script.ts" ```ts title="src/scripts/my-script.ts"
import { ExecArgs, IProductModuleService } from "@medusajs/types" import {
ExecArgs,
IProductModuleService,
} from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/utils" import { ModuleRegistrationName } from "@medusajs/utils"
export default async function myScript({ container }: ExecArgs) { export default async function myScript({ container }: ExecArgs) {
@@ -27,7 +30,8 @@ export default async function myScript({ container }: ExecArgs) {
ModuleRegistrationName.PRODUCT ModuleRegistrationName.PRODUCT
) )
const [, count] = await productModuleService.listAndCount() const [, count] = await productModuleService
.listAndCountProducts()
console.log(`You have ${count} product(s)`) console.log(`You have ${count} product(s)`)
} }
@@ -41,7 +45,7 @@ The function receives as a parameter an object having a `container` property, wh
To run the custom CLI script, run the Medusa CLI's `exec` command: To run the custom CLI script, run the Medusa CLI's `exec` command:
```bash npm2yarn ```bash
npx medusa exec ./src/scripts/my-script.ts npx medusa exec ./src/scripts/my-script.ts
``` ```
@@ -63,6 +67,6 @@ export default async function myScript({ args }: ExecArgs) {
Then, pass the arguments in the `exec` command after the file path: Then, pass the arguments in the `exec` command after the file path:
```bash npm2yarn ```bash
npx medusa exec ./src/scripts/my-script.ts arg1 arg2 npx medusa exec ./src/scripts/my-script.ts arg1 arg2
``` ```
@@ -4,11 +4,11 @@ export const metadata = {
# {metadata.title} # {metadata.title}
In this chapter, you’ll learn how to configure data model properties, such as setting their default value. In this chapter, you’ll learn how to configure data model properties.
## Property’s Default Value ## Property’s Default Value
Use the `default` method of the `model` utility to specify the default value of a property. Use the `default` method on a property's definition to specify the default value of a property.
For example: For example:
@@ -62,7 +62,7 @@ export default MyCustom
## Unique Property ## Unique Property
The `unique` method indicates that a property’s value must be unique in the database. The `unique` method indicates that a property’s value must be unique in the database through a unique index.
For example: For example:
@@ -8,7 +8,7 @@ In this chapter, you’ll learn how to define indices on a data model.
## Define Index on Property ## Define Index on Property
Define an index on a property using the `model` utility's `index` method. Use the `index` method on a property's definition to define an index.
For example: For example:
@@ -38,12 +38,12 @@ In this example, you define an index on the `name` property.
## Define Index on Data Model ## Define Index on Data Model
A data model has an `indexes` method that defines indices on the data model. A data model has an `indexes` method that defines indices on its properties.
The index can be on multiple columns (composite index). For example: The index can be on multiple columns (composite index). For example:
export const dataModelIndexHighlights = [ export const dataModelIndexHighlights = [
["7", "indexes", "Define indices on the data model."], ["7", "indexes", "Define indices on the data model's properties."],
["9", "on", "Specify the properties to define the index on."] ["9", "on", "Specify the properties to define the index on."]
] ]
@@ -0,0 +1,13 @@
export const metadata = {
title: `${pageNumber} Data Models`,
}
# {metadata.title}
In the next chapters, you'll learn more about creating data models, including property types, relationships, and more.
<Note type="soon" title="Important">
Data models are in active development and may change.
</Note>
@@ -25,8 +25,6 @@ const MyCustom = model.define("my_custom", {
export default MyCustom export default MyCustom
``` ```
By default, this property is considered to be the data model’s primary key.
--- ---
## text ## text
@@ -180,7 +178,7 @@ export default MyCustom
## array ## array
The `array` method defines an array or strings property. The `array` method defines an array of strings property.
For example: For example:
@@ -6,6 +6,12 @@ export const metadata = {
In this chapter, you’ll learn how to define relationships between data models in your module. In this chapter, you’ll learn how to define relationships between data models in your module.
<Note type="soon" title="Important">
Data model relationships are in active development and may change.
</Note>
## What is a Relationship Property? ## What is a Relationship Property?
A relationship property is defined using relation methods, such as `hasOne` or `belongsTo`. It represents a relationship between two data models in a module. A relationship property is defined using relation methods, such as `hasOne` or `belongsTo`. It represents a relationship between two data models in a module.
@@ -16,7 +16,7 @@ When the `q` filter is passed, the query is applied on searchable properties in
## Define a Searchable Property ## Define a Searchable Property
The `searchable` method of the `model` utility indicates that a `text` property is searchable. Use the `searchable` method on a `text` property to indicate that it's searchable.
For example: For example:
@@ -6,7 +6,7 @@ export const metadata = {
In this chapter, you'll learn how subscribers receive an event's data payload. In this chapter, you'll learn how subscribers receive an event's data payload.
## How to Access Data Payload ## Access Event's Data Payload
When events are emitted, they’re emitted with a data payload. When events are emitted, they’re emitted with a data payload.
@@ -40,6 +40,8 @@ export const config: SubscriberConfig = {
This logs the product ID received in the `product.created` event’s data payload to the console. This logs the product ID received in the `product.created` event’s data payload to the console.
---
## List of Events with Data Payload ## 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. Refer to [this reference](!resources!/events-reference) for a full list of events emitted by Medusa and their data payloads.
@@ -1,87 +0,0 @@
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 outside a module. 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 a 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" collapsibleLines="1-5" expandButtonLabel="Show Imports"
import { MedusaContainer } from "@medusajs/medusa"
import { IProductModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/utils"
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.
@@ -42,12 +42,11 @@ export default class HelloModuleService {
// ... // ...
} }
``` ```
### Loader ### 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. A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources.
For example: For example:
@@ -64,5 +63,4 @@ export default function helloWorldLoader({
logger.info("[helloWorldLoader]: Hello, World!") logger.info("[helloWorldLoader]: Hello, World!")
} }
``` ```
@@ -4,12 +4,12 @@ export const metadata = {
# {metadata.title} # {metadata.title}
In this chapter, you'll learn about how modules are isolated, and what that means for your custom development. In this chapter, you'll learn how modules are isolated, and what that means for your custom development.
<Note title="Summary"> <Note title="Summary">
- Modules can't access resources, such as services, from other modules. - Modules can't access resources, such as services, from other modules.
- You can use Medusa's tools, explained in the next chapters, to extend modules or implement features across modules. - You can use Medusa's tools, as explained in the next chapters, to extend a modules' features or implement features across modules.
</Note> </Note>
@@ -17,13 +17,13 @@ In this chapter, you'll learn about how modules are isolated, and what that mean
A module is unaware of any resources other than its own, such as services or data models. This means it can't access these resources if they're implemented in another module. A module is unaware of any resources other than its own, such as services or data models. This means it can't access these resources if they're implemented in another module.
For example, your custom module can't resolve the Product Module's main service or have direct relationships from its data model to another module's data model. For example, your custom module can't resolve the Product Module's main service or have direct relationships from its data model to the Product Module's data models.
--- ---
## Customize and Implement Features Across Modules ## How to Implement Custom Features Across Modules?
In your Medusa application, you want to implement features that span across modules, or you want to extend existing modules to add new features. In your Medusa application, you want to implement features that span across modules, or you want to extend an existing module's features and customize them for your own use case.
For example, you want to extend the Product Module to add new properties to the `Product` data model. For example, you want to extend the Product Module to add new properties to the `Product` data model.
@@ -1,23 +0,0 @@
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 has a link module that defines a relationship between the Product and Pricing modules. It links the `ProductVariant` and `PriceSet` data models.
![Diagram showcasing the link module between the Product and Pricing modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651569/Medusa%20Resources/product-pricing_vlxsiq.jpg)
Link modules create the relationship between modules while maintaining module isolation. The Medusa application only creates the link tables when both modules are available.
<Note type="soon">
Link modules are currently only available for Medusa’s commerce modules.
</Note>
@@ -0,0 +1,128 @@
export const metadata = {
title: `${pageNumber} Module Link`,
}
# {metadata.title}
In this chapter, you’ll learn what a module link is.
<Note type="soon" title="In Development">
Module links are in active development.
</Note>
## What is a Module Link?
A module link forms an association between two data models of different modules, while maintaining module isolation.
You can then retrieve data across the linked modules, and manage their linked records.
---
## Prerequisite: isQueryable Configuration
Before you define a module link, you must enable the `isQueryable` configuration of the module.
For example:
```js
module.exports = defineConfig({
// ...
modules: {
helloModuleService: {
resolve: "./modules/hello",
definition: {
isQueryable: true,
},
},
},
})
```
---
## How to Define a Module Link?
### 1. Create Link File
Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines the link using the `defineLink` function imported from `@medusajs/utils` and exports it.
For example:
export const highlights = [
["6", "linkable", "Special `linkable` property that holds the linkable data models of `HelloModule`."],
["7", "linkable", "Special `linkable` property that holds the linkable data models of `ProductModule`."],
]
```ts title="src/links/hello-product.ts" highlights={highlights}
import HelloModule from "../modules/hello"
import ProductModule from "@medusajs/product"
import { defineLink } from "@medusajs/utils"
export default defineLink(
HelloModule.linkable.myCustom,
ProductModule.linkable.product
)
```
The `defineLink` function accepts as parameters the link configurations of each module's data model. A module has a special `linkable` property that holds these configurations for its data models.
In this example, you define a module link between the `hello` module's `MyCustom` data model and the Product Module's `Product` data model.
### 2. Run Migrations
Medusa stores links as pivot tables in the database, so you must run migrations after defining a link:
```bash
npx medusa migrations run
```
---
## Define a List Link
By default, the defined link establishes a one-to-one relation: a record of a data model is linked to one record of the other data model.
To specify that a data model can have multiple of its records linked to the other data model's record, use the `isList` option.
For example:
```ts
import HelloModule from "../modules/hello"
import ProductModule from "@medusajs/product"
import { defineLink } from "@medusajs/utils"
export default defineLink(
{
model: HelloModule.linkable.myCustom,
isList: true
},
ProductModule.linkable.product
)
```
In this case, you pass an object of configuration as a parameter rather than the linked model. The object accepts the following properties:
- `model`: The data model to link.
- `isList`: Whether multiple records can be linked to one record of the other data model.
In this example, a record of `product` can be linked to more than one record of `myCustom`.
---
## Extend Data Models with Module Links
Module links are most useful when you want to add properties to a data model of another module.
For example, to add custom properties to the `Product` data model of the Product Module, you:
1. Create a module.
2. Create in the module a data model that holds the custom properties you want to add to the `Product` data model.
2. Define a module link that links your module to the Product Module.
Then, in the next chapters, you'll learn how to:
- Link each product to a record of your data model.
- Retrieve your data model's properties when you retrieve products.
@@ -80,7 +80,7 @@ The object that a module’s loaders receive as a parameter has an `options` pro
For example: For example:
```ts title="src/modules/hello/loaders/hello-world.ts" highlights={[["11"], ["16"]]} ```ts title="src/modules/hello/loaders/hello-world.ts" highlights={[["11"], ["12", "ModuleOptions", "The type of expected module options."], ["16"]]}
import { import {
LoaderOptions, LoaderOptions,
} from "@medusajs/modules-sdk" } from "@medusajs/modules-sdk"
@@ -6,9 +6,15 @@ export const metadata = {
In this chapter, you’ll learn what the remote link is and how to use it to manage links. In this chapter, you’ll learn what the remote link is and how to use it to manage links.
<Note type="soon" title="In Development">
Remote Links are in active development.
</Note>
## What is the Remote Link? ## What is the Remote Link?
The remote link is a class with utility methods to manage links defined by the link module. It’s registered in the Medusa container under the `remoteLink` registration name. The remote link is a class with utility methods to manage links between data models. It’s registered in the Medusa container under the `remoteLink` registration name.
For example: For example:
@@ -38,7 +44,9 @@ export async function POST(
You can use its methods to manage links, such as create or delete links. You can use its methods to manage links, such as create or delete links.
### Create Link ---
## Create Link
To create a link between records of two data models, use the `create` method of the remote link. To create a link between records of two data models, use the `create` method of the remote link.
@@ -51,23 +59,25 @@ import { Modules } from "@medusajs/utils"
await remoteLink.create({ await remoteLink.create({
[Modules.PRODUCT]: { [Modules.PRODUCT]: {
variant_id: product.variants[0].id, product_id: "prod_123",
}, },
[Modules.PRICING]: { "hello": {
price_set_id: price.id, my_custom_id: "mc_123",
}, },
}) })
``` ```
The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules. The `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. The value of each module’s property is an object, whose keys are of the format `{data_model_snake_name}_id`, and values are the IDs of the linked record.
So, in the example above, you 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. So, in the example above, you link a record of the `MyCustom` data model in a `hello` module to a `Product` record in the Product Module.
### 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. ## Dismiss Link
To remove a link between records of two data models, use the `dismiss` method of the remote link.
For example: For example:
@@ -78,19 +88,21 @@ import { Modules } from "@medusajs/utils"
await remoteLink.dismiss({ await remoteLink.dismiss({
[Modules.PRODUCT]: { [Modules.PRODUCT]: {
variant_id: product.variants[0].id, product_id: "prod_123",
}, },
[Modules.PRICING]: { "hello": {
price_set_id: price.id, my_custom_id: "mc_123",
}, },
}) })
``` ```
The `dismiss` method accepts the same parameter type as the [create method](#create-link). The `dismiss` method accepts the same parameter type as the [create method](#create-link).
### 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. ## Cascade Delete Linked Records
If a record is deleted, use the `delete` method of the remote link to delete all linked records.
For example: For example:
@@ -103,16 +115,18 @@ await productModuleService.deleteVariants([variant.id])
await remoteLink.delete({ await remoteLink.delete({
[Modules.PRODUCT]: { [Modules.PRODUCT]: {
variant_id: variant.id, product_id: "prod_123",
}, },
}) })
``` ```
This deletes all records linked to the deleted variant with cascade delete enabled in their relationship. This deletes all records linked to the deleted product.
### 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. ## Restore Linked Records
If a record that was previously soft-deleted is now restored, use the `restore` method of the remote link to restore all linked records.
For example: For example:
@@ -121,77 +135,11 @@ import { Modules } from "@medusajs/utils"
// ... // ...
await productModuleService.restoreVariants([variant.id]) await productModuleService.restoreProducts(["prod_123"])
await remoteLink.restore({ await remoteLink.restore({
[Modules.PRODUCT]: { [Modules.PRODUCT]: {
variant_id: variant.id, product_id: "prod_123",
}, },
}) })
``` ```
---
## Link Module's Service
The remote link has a `getLinkModule` method to retrieve the service of the link module. This service has `list` and `retrieve` methods to retrieve the linked items.
For example, to retrieve the link module of the Product and Pricing modules:
export const linkModuleServiceHighlights = [
["6", "Modules.PRODUCT", "The name of the first module in the link module's definition."],
["7", '"variant_id"', "The foreign key that links to the record in the first module."],
["8", "Modules.PRICING", "The name of the second module in the link module's definition."],
["9", '"price_set_id"', "The foreign key that links to the record in the second module."],
["12", "", "The link module's service is undefined if either of the modules isn't installed or there's no link module with the specified definition."]
]
```ts highlights={linkModuleServiceHighlights}
import { Modules } from "@medusajs/utils"
// ...
const linkModuleService = remoteLink.getLinkModule(
Modules.PRODUCT,
"variant_id",
Modules.PRICING,
"price_set_id"
)
if (!linkModuleService) {
return
}
```
The `getLinkModule` method accepts four parameter:
1. A string indicating the name of the first module in the link module's definition.
2. A string indicating the foreign key that links to the record in the first module.
3. A string indicating the name of the second module in the link module's definition.
4. A string indicating the foreign key that links to the record in the second module.
Notice that the returned link module service might be undefined if either of the modules isn't installed, or if there's no link module with the specified definition.
### List Linked Items
The link module's service has a `list` method that retrieves a list of linked records. It also accepts filters to retrieve specific linked items.
For example, to retrieve the price sets linked to a variant:
```ts
import { Modules } from "@medusajs/utils"
// ...
const linkModuleService = remoteLink.getLinkModule(
Modules.PRODUCT,
"variant_id",
Modules.PRICING,
"price_set_id"
)
const items = await linkModuleService.list(
{ variant_id: [variant.id] },
{ select: ["variant_id", "price_set_id"] }
)
```
@@ -14,27 +14,11 @@ The remote query fetches data across modules. It’s a function registered in th
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. 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.
--- <Note type="check">
## isQueryable Configuration - [Enable the isQueryable configuration of the module](../module-links/page.mdx#prerequisite-isqueryable-configuration)
Before you use remote query on your module, you must enable the `isQueryable` configuration of the module. </Note>
For example:
```js
module.exports = defineConfig({
// ...
modules: {
helloModuleService: {
resolve: "./modules/hello",
definition: {
isQueryable: true,
},
},
},
})
```
--- ---
@@ -93,6 +77,48 @@ You then pass the query to the `remoteQuery` function to retrieve the results.
--- ---
## Retrieve Linked Records
Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`.
For example:
```ts highlights={[["6"]]}
const query = remoteQueryObjectFromString({
entryPoint: "my_custom",
fields: [
"id",
"name",
"product.*"
],
})
```
<Note title="Tip">
`.*` means that all of data model's properties should be retrieved. To retrieve a specific property, replace the `*` with the property's name. For example, `product.title`.
</Note>
### Retrieve List Link Records
If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`.
For example:
```ts highlights={[["6"]]}
const query = remoteQueryObjectFromString({
entryPoint: "my_custom",
fields: [
"id",
"name",
"products.*"
],
})
```
---
## Apply Filters ## Apply Filters
```ts highlights={[["6"], ["7"], ["8"], ["9"]]} ```ts highlights={[["6"], ["7"], ["8"], ["9"]]}
@@ -259,6 +285,8 @@ The remote query function alternatively accepts a string with GraphQL syntax as
} }
` `
const result = await remoteQuery(query)
res.json({ res.json({
my_customs: result, my_customs: result,
}) })
@@ -12,9 +12,9 @@ In this chapter, you’ll learn about what the service factory is and how to use
Medusa provides a service factory that your module’s main service can extend. Medusa provides a service factory that your module’s main service can extend.
The service factory generates data management methods for your data models, so you don't have to implement them manually. The service factory generates data management methods for your data models, so you don't have to implement these methods manually.
<Note title="Use the service factory when" type="success"> <Note title="Extend the service factory when" type="success">
- Your service provides data-management functionalities of your data models. - Your service provides data-management functionalities of your data models.
@@ -50,7 +50,7 @@ export default HelloModuleService
The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for. The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for.
In the example above, the `HelloModuleService` now has methods to manage the `MyCustom` data model, such as `createMyCustoms`. In the example above, since the `HelloModuleService` extends `MedusaService`, it has methods to manage the `MyCustom` data model, such as `createMyCustoms`.
### Generated Methods ### Generated Methods
@@ -4,15 +4,12 @@ export const metadata = {
# {metadata.title} # {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. 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 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: 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. - Expose API routes with control over authentication.
- Manage data models in services. - Build sophisticated business logic in modules and manage links between them.
- Create relationships between modules.
- Create data models with complex fields and relations.
- Create loaders outside of modules.
- Access events payloads.
- Create advanced workflows and configure retries and timeout. - Create advanced workflows and configure retries and timeout.
- Add new pages to the Medusa Admin. - Add new pages to the Medusa Admin.
- Do more with subscribers, scheduled jobs, and other tools.
@@ -4,9 +4,9 @@ export const metadata = {
# {metadata.title} # {metadata.title}
In this document, you’ll learn how to access errors that occur during a workflow’s execution. In this chapter, you’ll learn how to access errors that occur during a workflow’s execution.
## How to Access Workflow Errors ## How to Access Workflow Errors?
By default, when an error occurs in a workflow, it throws that error, and the execution stops. By default, when an error occurs in a workflow, it throws that error, and the execution stops.
@@ -23,9 +23,10 @@ Start by creating the file `src/workflows/update-product-erp/index.ts` that will
In the file, add the type of the expected workflow input: In the file, add the type of the expected workflow input:
```ts title="src/workflows/update-product-erp/index.ts" ```ts title="src/workflows/update-product-erp/index.ts"
import { UpdateProductDTO } from "@medusajs/types" import { UpsertProductDTO } from "@medusajs/types"
export type UpdateProductAndErpWorkflowInput = UpsertProductDTO
export type UpdateProductAndErpWorkflowInput = UpdateProductDTO
``` ```
The expected input is the data to update in the product along with the product’s ID. The expected input is the data to update in the product along with the product’s ID.
@@ -40,17 +41,9 @@ Create the file `src/workflows/update-product-erp/steps/update-product.ts` with
export const updateProductHighlights = [ export const updateProductHighlights = [
["13", "resolve", "Resolve the `ProductService` from the Medusa container."], ["13", "resolve", "Resolve the `ProductService` from the Medusa container."],
[ ["16", "previousProductData", "Retrieve the `previousProductData` to pass it to the compensation function."],
"16", ["19", "updateProducts", "Update the product."],
"previousProductData", ["39", "updateProducts", "Revert the product’s data using the `previousProductData` passed from the step to the compensation function."]
"Retrieve the `previousProductData` to pass it to the compensation function.",
],
["19", "", "Update the product."],
[
"39",
"",
"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} collapsibleLines="1-9" expandButtonLabel="Show Imports" ```ts title="src/workflows/update-product-erp/steps/update-product.ts" highlights={updateProductHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
@@ -66,9 +59,10 @@ const updateProduct = createStep(
context.container.resolve(ModuleRegistrationName.PRODUCT) context.container.resolve(ModuleRegistrationName.PRODUCT)
const { id } = input const { id } = input
const previousProductData = await productModuleService.retrieve(id) const previousProductData =
await productModuleService.retrieveProduct(id)
const product = await productModuleService.update(id, input) const product = await productModuleService.updateProducts(id, input)
return new StepResponse(product, { return new StepResponse(product, {
// pass to compensation function // pass to compensation function
@@ -82,7 +76,9 @@ const updateProduct = createStep(
const { id, type, options, variants, ...previousData } = previousProductData const { id, type, options, variants, ...previousData } = previousProductData
await productModuleService.update(id, { await productModuleService.updateProducts(
id,
{
...previousData, ...previousData,
variants: variants.map((variant) => { variants: variants.map((variant) => {
const variantOptions = {} const variantOptions = {}
@@ -110,7 +106,7 @@ export default updateProduct
In the step: In the step:
- You resolve the `ProductService` from the Medusa container. - You resolve the Product Module's main service from the Medusa container.
- You retrieve the `previousProductData` to pass it to the compensation function. - You retrieve the `previousProductData` to pass it to the compensation function.
- You update and return the product. - You update and return the product.
@@ -211,11 +207,11 @@ Change the content of `src/workflows/update-product-erp/index.ts` to the followi
```ts title="src/workflows/update-product-erp/index.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" ```ts title="src/workflows/update-product-erp/index.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { createWorkflow } from "@medusajs/workflows-sdk" import { createWorkflow } from "@medusajs/workflows-sdk"
import { UpdateProductDTO, ProductDTO } from "@medusajs/types" import { UpsertProductDTO, ProductDTO } from "@medusajs/types"
import updateProduct from "./steps/update-product" import updateProduct from "./steps/update-product"
import updateErp from "./steps/update-erp" import updateErp from "./steps/update-erp"
export type UpdateProductAndErpWorkflowInput = UpdateProductDTO export type UpdateProductAndErpWorkflowInput = UpsertProductDTO
type WorkflowOutput = { type WorkflowOutput = {
product: ProductDTO product: ProductDTO
@@ -8,7 +8,7 @@ In this chapter, you'll learn how to add a compensation function to a step.
## Compensation Function ## Compensation Function
Errors can occur in a workflow. To avoid data inconsistency, define a function to run when an error occurs in a step. This function is called the compensation function. To avoid data inconsistency when an error is thrown in a workflow, define a function (called a compensation function) and pass it as a second parameter to the `createStep` function.
For example: For example:
@@ -34,9 +34,11 @@ const step1 = createStep(
) )
``` ```
Each step can have a compensation 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. Each step can have a compensation 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.
### Test Compensation Function ---
## Test the Compensation Function
1. Add another step that throws an error: 1. Add another step that throws an error:
@@ -107,7 +109,7 @@ npm run dev
5. Send a `GET` request to `/store/workflow`: 5. Send a `GET` request to `/store/workflow`:
```bash apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/workflows" ```bash apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/workflow"
curl http://localhost:9000/store/workflow curl http://localhost:9000/store/workflow
``` ```
@@ -4,7 +4,7 @@ export const metadata = {
# {metadata.title} # {metadata.title}
This chapter lists some constraints to keep in mind when defining Workflow constructor functions. This chapter lists some constraints to keep in mind when defining a workflow's constructor function.
## No Arrow Functions ## No Arrow Functions
@@ -28,6 +28,8 @@ const myWorkflow = createWorkflow<
}) })
``` ```
---
## No Async Functions ## No Async Functions
The function passed to the `createWorkflow` can’t be an async function: The function passed to the `createWorkflow` can’t be an async function:
@@ -50,6 +52,8 @@ const myWorkflow = createWorkflow<
}) })
``` ```
---
## No Direct Data Manipulation ## 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: Since the constructor function only defines how the workflow works, you can’t directly manipulate data within the function. Instead, use the `transform` function:
@@ -22,11 +22,15 @@ A workflow is considered long-running if at least one step has its `async` confi
For example, consider the following workflow and steps: For example, consider the following workflow and steps:
```ts title="src/workflows/hello-world.ts" highlights={[["13"]]} collapsibleLines="1-10" expandButtonLabel="Show More" ```ts title="src/workflows/hello-world.ts" highlights={[["14"]]} collapsibleLines="1-10" expandButtonLabel="Show More"
import { createStep, createWorkflow } from "@medusajs/workflows-sdk" import {
createStep,
createWorkflow,
StepResponse,
} from "@medusajs/workflows-sdk"
const step1 = createStep("step-1", async () => { const step1 = createStep("step-1", async () => {
// ... return new StepResponse({})
}) })
const step2 = createStep( const step2 = createStep(
@@ -35,74 +39,90 @@ const step2 = createStep(
async: true, async: true,
}, },
async () => { async () => {
// ... return new StepResponse({})
} }
) )
const step3 = createStep("step-3", async () => { const step3 = createStep("step-3", async () => {
// ... return new StepResponse("Finished three steps")
}) })
type WorkflowOutput = { type WorkflowOutput = {
message: string message: string
} }
const myWorkflow = createWorkflow<{}, WorkflowOutput>( const myWorkflow = createWorkflow<
{ {},
name: "hello-world", WorkflowOutput
}, >("hello-world", function () {
function () {
step1() step1()
step2() step2()
step3() const message = step3()
return {
message,
} }
) })
export default myWorkflow export default myWorkflow
``` ```
The second step has in its configuration object `async` set to true. This indicates that this step is an asynchronous step. The second step has in its configuration object `async` set to true. This indicates that this step is an asynchronous step.
<Note title="Important">
An asynchronous step must return for the execution to continue.
</Note>
So, when you execute the `hello-world` workflow, it continues its execution in the background once it reaches the second step. So, when you execute the `hello-world` workflow, it continues its execution in the background once it reaches the second step.
--- ---
## Access Long-Running Workflow Status and Result ## Access Long-Running Workflow Status and Result
<Note type="check">
- [A workflow engine module installed](!resources!/architectural-modules/workflow-engine/in-memory).
</Note>
To access the status and result of a long-running workflow, use the workflow engine registered in the Medusa Container. The workflow engine provides methods to access and subscribe to workflow executions. To access the status and result of a long-running workflow, use the workflow engine registered in the Medusa Container. The workflow engine provides methods to access and subscribe to workflow executions.
For example: For example:
export const highlights = [ export const highlights = [
["18", "", "Resolve the workflow engine from the Medusa container."], ["18", "resolve", "Resolve the workflow engine from the Medusa container."],
["24", "subscribe", "Subscribe to status changes of the workflow execution."], ["30", "subscribe", "Subscribe to status changes of the workflow execution."],
] ]
```ts title="src/api/store/workflows/route.ts" highlights={highlights} collapsibleLines="1-11" expandButtonLabel="Show Imports" ```ts title="src/api/store/workflows/route.ts" highlights={highlights} collapsibleLines="1-11" expandButtonLabel="Show Imports"
import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa" import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
import myWorkflow from "../../../workflows/hello-world" import myWorkflow from "../../../workflows/hello-world"
import { IWorkflowEngineService } from "@medusajs/workflows-sdk" import {
IWorkflowEngineService,
} from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/utils" import { ModuleRegistrationName } from "@medusajs/utils"
export async function GET(req: MedusaRequest, res: MedusaResponse) { export async function GET(req: MedusaRequest, res: MedusaResponse) {
const { transaction, result } = await myWorkflow(req.scope).run() const { transaction, result } = await myWorkflow(req.scope).run()
const workflowEngine = req.scope.resolve<IWorkflowEngineService>( const workflowEngineModuleService = req.scope.resolve<
IWorkflowEngineService
>(
ModuleRegistrationName.WORKFLOW_ENGINE ModuleRegistrationName.WORKFLOW_ENGINE
) )
await workflowEngine.subscribe({ const subscriptionOptions = {
workflowId: "hello-world", workflowId: "hello-world",
transactionId: transaction.transactionId, transactionId: transaction.transactionId,
subscriber: (data) => { subscriberId: "hello-world-subscriber",
}
await workflowEngineModuleService.subscribe({
...subscriptionOptions,
subscriber: async (data) => {
if (data.eventType === "onFinish") { if (data.eventType === "onFinish") {
console.log("Finished execution", data.result) console.log("Finished execution", data.result)
// unsubscribe
await workflowEngineModuleService.unsubscribe({
...subscriptionOptions,
subscriberOrId: subscriptionOptions.subscriberId,
})
} else if (data.eventType === "onStepFailure") { } else if (data.eventType === "onStepFailure") {
console.log("Workflow failed", data.step) console.log("Workflow failed", data.step)
} }
@@ -141,3 +161,5 @@ The `subscribe` method accepts an object having three properties:
/> />
Once the workflow execution finishes, the subscriber function is executed with the `eventType` of the received parameter set to `onFinish`. The workflow’s output is set in the `result` property of the parameter. Once the workflow execution finishes, the subscriber function is executed with the `eventType` of the received parameter set to `onFinish`. The workflow’s output is set in the `result` property of the parameter.
You can unsubscribe from the workflow using the workflow engine's `unsubscribe` method, which requires the same object parameter as the `subscribe` method.
@@ -8,9 +8,9 @@ In this chapter, you’ll learn how to run workflow steps in parallel.
## parallelize Utility Function ## 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. If your workflow has steps that don’t rely on one another’s results, run them in parallel using the `parallelize` utility function imported from the `@medusajs/workflows-sdk`.
The `parallelize` utility function imported from the `@medusajs/workflows-sdk` package allows you to run the steps in parallel. The workflow waits until all steps passed to the `parallelize` function finish executing before continuing with the rest of its implementation.
For example: For example:
@@ -55,4 +55,4 @@ const myWorkflow = createWorkflow<
The `parallelize` function accepts the steps to run in parallel as a parameter. 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. It returns an array of the steps' results. The results are ordered based on the `parallelize` parameters' order.
@@ -10,12 +10,13 @@ In this chapter, you’ll learn how to configure steps to allow retrial on failu
By default, when an error occurs in a step, the step and the workflow fail, and the execution stops. By default, when an error occurs in a step, the step and the workflow fail, and the execution stops.
You can configure the step to retry on failure. The `createStep` function can accept a configuration object instead of the step’s name as a first parameter: You can configure the step to retry on failure. The `createStep` function can accept a configuration object instead of the step’s name as a first parameter.
```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" For example:
```ts title="src/workflows/hello-world.ts" highlights={[["9"]]} collapsibleLines="1-5" expandButtonLabel="Show Imports"
import { import {
createStep, createStep,
StepResponse,
createWorkflow, createWorkflow,
} from "@medusajs/workflows-sdk" } from "@medusajs/workflows-sdk"
@@ -1,12 +0,0 @@
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 different types of architectural modules, including cache and event modules.
@@ -22,7 +22,7 @@ There are different architectural module types including:
- Cache Module: Defines the caching mechanism or logic to cache computational results. - Cache Module: Defines the caching mechanism or logic to cache computational results.
- Event Module: Integrates a pub/sub service to handle subscribing to and emitting events. - Event Module: Integrates a pub/sub service to handle subscribing to and emitting events.
- File Module: Integrates a storage service to handle uploading files. - File Module: Integrates a storage service to handle uploading and managing files.
- Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers. - Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers.
--- ---
@@ -10,9 +10,9 @@ In this chapter, you’ll learn how to customize the Medusa Admin dashboard.
The Medusa Admin is an admin dashboard that merchants use to manage their store's data. The Medusa Admin is an admin dashboard that merchants use to manage their store's data.
You can extend the Medusa Admin to add widgets and new pages. Your customizations interact with API routes to provide merchants with custom functionalities. You can extend the Medusa Admin to add widgets and new pages. In your customizations, you interact with API routes to provide merchants with custom functionalities.
The Medusa Admin is installed in your Medusa application and runs at the path `/app` when you start the Medusa application. The Medusa Admin is installed in your Medusa application and runs at `http://localhost:9000/app` when you start the application.
--- ---
@@ -34,13 +34,13 @@ const ProductWidget = () => {
} }
export const config = defineWidgetConfig({ export const config = defineWidgetConfig({
zone: "product.details.after", zone: "product.details.before",
}) })
export default ProductWidget export default ProductWidget
``` ```
This inserts a widget with the text “Product Widget” at the end of a product’s details page. This inserts a widget with the text “Product Widget” at the beginning of a product’s details page.
### Test the Widget ### Test the Widget
@@ -50,4 +50,4 @@ To test out the widget, start the Medusa application:
npm run dev npm run dev
``` ```
Then, open a product’s details page. You’ll find your custom widget at the bottom of the page. Then, open a product’s details page in the Medusa Admin. You’ll find your custom widget at the top of the page.
+4 -4
View File
@@ -14,11 +14,11 @@ The Medusa core application provides a set of admin and store API routes out-of-
--- ---
## How to Create an API Route ## 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`. 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 exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). Each file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…).
For example, to create a `GET` API Route at `/store/hello-world`, create the file `src/api/store/hello-world/route.ts` with the following content: 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:
@@ -58,6 +58,6 @@ curl http://localhost:9000/store/hello-world
<Note title="Use API routes when" type="success"> <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. - You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application.
</Note> </Note>
@@ -18,7 +18,7 @@ Refer to [this reference](!resources!/commerce-modules) for a full list of comme
## Resolve Commerce Module Services ## 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. 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 routes, to use its functionality.
For example, you saw this code snippet in the [Medusa container chapter](../medusa-container/page.mdx): For example, you saw this code snippet in the [Medusa container chapter](../medusa-container/page.mdx):
+16 -9
View File
@@ -12,17 +12,17 @@ A data model is a class that represents a table in the database. It's created in
--- ---
## How to Create a Data Model ## How to Create a Data Model?
<Note title="Steps Summary"> <Note title="Steps Summary">
1. Create data model class in a module. 1. Create a data model in a module.
2. Generate migration for the data model. 2. Create migration for the data model.
4. Run migration to add table for data model in the database. 4. Run migration to add the data model's table in the database.
</Note> </Note>
A data model is created in a TypeScript or JavaScript file under a module's `models` directory. It's defined using the `model` utility imported from `@medjusajs/utils`. A data model is created in a TypeScript or JavaScript file under a module's `models` directory. It's defined using the `model` utility imported from `@medusajs/utils`.
For example, create the file `src/modules/hello/models/my-custom.ts` with the following content: For example, create the file `src/modules/hello/models/my-custom.ts` with the following content:
@@ -48,8 +48,6 @@ The example above defines the data model `MyCustom` with the properties `id` and
A migration defines changes to be made in the database, such as create or update tables. A migration defines changes to be made in the database, such as create or update tables.
So, you must create a migration that creates a table for your data model in the database.
A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It has two methods: A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It has two methods:
- The `up` method reflects changes on the database. - The `up` method reflects changes on the database.
@@ -74,6 +72,15 @@ A migration is a class created in a TypeScript or JavaScript file under a module
}) })
``` ```
<Note title="Important">
The `databaseName`'s value is only used to generate the migrations. So, make sure it:
1. Is unique to the module. Two modules shouldn't have the same database name, and it shouldn't be the same as the Medusa application.
2. Doesn't change between migrations, as that causes inconsistencies between the migrations.
</Note>
2. Run the following command in the root directory of your Medusa application: 2. Run the following command in the root directory of your Medusa application:
```bash ```bash
@@ -95,7 +102,7 @@ For example:
```ts title="src/modules/migrations/Migration20240429090012.ts" ```ts title="src/modules/migrations/Migration20240429090012.ts"
import { Migration } from "@mikro-orm/migrations" import { Migration } from "@mikro-orm/migrations"
export class Migration20240624145652 extends Migration { export class Migration20240702105919 extends Migration {
async up(): Promise<void> { async up(): Promise<void> {
this.addSql("create table if not exists \"my_custom\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"my_custom_pkey\" primary key (\"id\"));") this.addSql("create table if not exists \"my_custom\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"my_custom_pkey\" primary key (\"id\"));")
@@ -120,7 +127,7 @@ The queries performed in each of the methods use PostgreSQL syntax.
To reflect the changes in the migration, run the `migration` command: To reflect the changes in the migration, run the `migration` command:
```bash npm2yarn ```bash
npx medusa migrations run npx medusa migrations run
``` ```
@@ -14,9 +14,11 @@ When an action is performed in Medusa, such as creating a product, the Medusa ap
## What is a Subscriber? ## What is a Subscriber?
A subscriber is a function executed whenever its associated event is emitted. A subscriber is a function executed whenever the event it listens to is emitted.
The subscriber is created in a TypeScript or JavaScript file under the `src/subscribers` directory. ### How to Create a Subscriber?
A 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: For example, create the file `src/subscribers/product-created.ts` with the following content:
@@ -36,13 +38,32 @@ export const config: SubscriberConfig = {
A subscriber file must export: A subscriber file must export:
- The subscriber function that is an asynchronous function executed whenever the associated event is triggered. - A subscriber function that is an asynchronous function executed whenever the associated event is triggered.
- A configuration object defining the event this subscriber is listening to. - A configuration object defining the event this subscriber is listening to.
The above subscriber listens to the `product.created` event. Whenever the event is emitted, it logs in the terminal `A product is created`. The above subscriber listens to the `product.created` event. Whenever the event is emitted, it logs in the terminal `A product is created`.
--- ---
## Test the Subscriber
To test the subscriber, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, go to the Medusa Admin at `localhost:9000/app` and create a product. You’ll see the following messages logged in the Medusa application's terminal:
```bash
info: Processing product.created which has 1 subscribers
A product was 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 Subscribers ## When to Use Subscribers
<Note title="Use subscribers when" type="success"> <Note title="Use subscribers when" type="success">
@@ -59,16 +80,10 @@ The subscriber function accepts an object parameter with the property `container
For example: For example:
{/* TODO change how event names are loaded */}
export const highlights = [ export const highlights = [
["10", "container", "Recieve the Medusa Container in the object parameter."], ["10", "container", "Recieve the Medusa Container in the object parameter."],
["13", "resolve", "Resolve the Product Module's main service."], ["13", "resolve", "Resolve the Product Module's main service."],
[ ["13", "ModuleRegistrationName.PRODUCT", "The module's registration name imported from `@medusajs/modules-sdk`."]
"13",
"ModuleRegistrationName.PRODUCT",
"The resource registration name imported from `@medusajs/modules-sdk`.",
],
] ]
```ts title="src/subscribers/product-created.ts" highlights={highlights} ```ts title="src/subscribers/product-created.ts" highlights={highlights}
@@ -79,12 +94,15 @@ import { ModuleRegistrationName } from "@medusajs/utils"
export default async function productCreateHandler({ export default async function productCreateHandler({
data, data,
container, container,
}: SubscriberArgs<Record<string, string>>) { }: SubscriberArgs<{ id: string }>) {
const productModuleService: IProductModuleService = container.resolve( const productModuleService: IProductModuleService =
ModuleRegistrationName.PRODUCT container.resolve(ModuleRegistrationName.PRODUCT)
)
const product = await productModuleService.retrieve(data.id) const productId = "data" in data ? data.data.id : data.id
const product = await productModuleService.retrieveProduct(
productId
)
console.log(`The product ${product.title} was created`) console.log(`The product ${product.title} was created`)
} }
+8 -12
View File
@@ -8,9 +8,7 @@ In this chapter, you’ll learn about loaders and how to use them.
## What is a Loader? ## What is a Loader?
A loader is a function executed when the Medusa application starts. A loader is a function executed when the Medusa application starts. You define and export it in a module.
A module can define loaders to execute on application start-up.
--- ---
@@ -30,25 +28,23 @@ export default function helloWorldLoader() {
### Export Loader in Module Definition ### Export Loader in Module Definition
You must export your loaders in the module definition. Import the loader in `src/modules/hello/index.ts` and export it in the module's definition:
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" ```ts title="src/modules/hello/index.ts"
// other imports... // other imports...
import helloWorldLoader from "./loaders/hello-world" import helloWorldLoader from "./loaders/hello-world"
// ... export default Module("hello", {
export default {
// ... // ...
loaders: [helloWorldLoader], loaders: [helloWorldLoader],
} })
``` ```
The value of the `loaders` property is an array of loader functions. The value of the `loaders` property is an array of loader functions.
### Test Loader ---
## Test the Loader
Start the Medusa application: Start the Medusa application:
@@ -75,6 +71,6 @@ Among the messages logged in the terminal, you’ll see the following message:
<Note title="Don't use loaders if" type="error"> <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. - You want to perform an action continuously or at a set time pattern in the application. Use scheduled jobs instead, which is explained in an upcoming chapter.
</Note> </Note>
@@ -33,7 +33,8 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
ModuleRegistrationName.PRODUCT ModuleRegistrationName.PRODUCT
) )
const [, count] = await productModuleService.listAndCount() const [, count] = await productModuleService
.listAndCountProducts()
res.json({ res.json({
count, count,
@@ -14,7 +14,7 @@ Use modules to customize or develop commerce and architectural features in your
--- ---
## How to Create a Module ## How to Create a Module?
<Note title="Steps Summary"> <Note title="Steps Summary">
@@ -28,7 +28,7 @@ Modules are created in a sub-directory of `src/modules`.
For example, create the directory `src/modules/hello`. For example, create the directory `src/modules/hello`.
### 1. Create a Service ### 1. Create Main Service
A module must define a service. A service is a TypeScript or JavaScript class holding methods related to a business logic or commerce functionality. A module must define a service. A service is a TypeScript or JavaScript class holding methods related to a business logic or commerce functionality.
@@ -46,21 +46,27 @@ export default class HelloModuleService {
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. 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: 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."]]} ```ts title="src/modules/hello.index.ts" highlights={[["5", "", "The main service of the module."]]}
import HelloModuleService from "./service" import HelloModuleService from "./service"
import { Module } from "@medusajs/utils"
export default { export default Module("hello", {
service: HelloModuleService, service: HelloModuleService,
} })
``` ```
You use the `Module` function imported from `@medusajs/utils` to create the module definition. It requires two parameters:
1. The module's name.
2. An object with a required property `service` indicating the module's main service.
### 3. Add Module to Configurations ### 3. Add Module to Configurations
The last step is to add the module in Medusa’s configurations. The last step is to add the module in Medusa’s configurations.
In `medusa-config.js`, add the module to the `modules` object: In `medusa-config.js`, add a `modules` property and pass to it your custom module:
```js title="medusa-config.js" highlights={[["4", "helloModuleService", "The key of the main service to be registered in the Medusa container."]]} ```js title="medusa-config.js" highlights={[["4", "helloModuleService", "The key of the main service to be registered in the Medusa container."]]}
module.exports = defineConfig({ module.exports = defineConfig({
@@ -75,23 +81,25 @@ module.exports = defineConfig({
Its key (`helloModuleService`) is the name of the module’s main service. It will be registered in the Medusa container with that name. Its key (`helloModuleService`) is the name of the module’s main service. It will be registered in the Medusa container with that name.
Its value is an object having the `resolve` property, whose value is either a path to module's directory (relative to `src`, so it shouldn't include `src` in the path) or an `npm` package’s name. Its value is an object having the `resolve` property, whose value is either a path to module's directory relative to `src`(it shouldn't include `src` in the path), or an `npm` package’s name.
### 4. Test the Module ---
## 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. 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: For example, create the API route `src/api/store/custom/route.ts` with the following content:
```ts title="src/api/store/custom/route.ts" ```ts title="src/api/store/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa" import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
import HelloService from "../../../modules/hello/service" import HelloModuleService from "../../../modules/hello/service"
export async function GET( export async function GET(
req: MedusaRequest, req: MedusaRequest,
res: MedusaResponse res: MedusaResponse
): Promise<void> { ): Promise<void> {
const helloModuleService: HelloService = req.scope.resolve( const helloModuleService: HelloModuleService = req.scope.resolve(
"helloModuleService" "helloModuleService"
) )
@@ -128,7 +136,6 @@ You’ll receive the following response:
<Note title="Use modules when" type="success"> <Note title="Use modules when" type="success">
- You're implementing a custom commerce feature. For example, you're implementing digital products. - 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. - 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.
</Note> </Note>
+2 -2
View File
@@ -4,11 +4,11 @@ export const metadata = {
# {metadata.title} # {metadata.title}
In the next chapters, you’ll learn about the basic concepts of Medusa. These concepts are central to your Medusa development. In the next chapters, you’ll learn about the basic concepts of Medusa that are central in your development.
By the end of these chapter, you’ll be able to: By the end of these chapter, you’ll be able to:
- Expose your custom functionalities through REST APIs. - Expose your custom functionalities through endpoints.
- Create custom modules that define custom business logic. - Create custom modules that define custom business logic.
- Create custom data models. - Create custom data models.
- Execute scripts when the Medusa application starts. - Execute scripts when the Medusa application starts.
@@ -1,10 +1,10 @@
export const metadata = { export const metadata = {
title: `${pageNumber} Important Directories and Files`, title: `${pageNumber} Project Directories and Files`,
} }
# {metadata.title} # {metadata.title}
In this chapter, you’ll learn about important directories and files in your Medusa application's codebase. In this chapter, you’ll learn about important directories and files in your Medusa application's project.
## src ## src
@@ -13,10 +13,11 @@ This directory is the central place for your custom development. It includes the
- `admin`: Holds your admin dashboard's custom components and pages. - `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. - `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. - `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. - `modules`: Holds your custom modules that implement custom business logic.
- `scripts`: Holds your custom scripts to be executed using Medusa's CLI tool.
- `subscribers`: Holds your event listeners that are executed asynchronously whenever an event is emitted. - `subscribers`: Holds your event listeners that are executed asynchronously whenever an event is emitted.
- `workflows`: Holds your custom flows that can be executed from anywhere in your application.
## medusa-config.js ## medusa-config.js
This file holds your Medusa configurations, such as your PostgreSQL database configurations or your Medusa Admin plugin configurations. This file holds your Medusa configurations, such as your PostgreSQL database configurations.
@@ -28,7 +28,7 @@ export default function () {
// the job's configurations // the job's configurations
export const config = { export const config = {
name: "hello-world", name: "every-minute-message",
// execute every minute // execute every minute
schedule: "* * * * *", schedule: "* * * * *",
} }
@@ -37,10 +37,10 @@ export const config = {
A scheduled job file must export: A scheduled job file must export:
- The function to be executed whenever it’s time to run the scheduled job. - A function to be executed whenever it’s time to run the scheduled job.
- A configuration object defining the job. It has two properties: - A configuration object defining the job. It has two properties:
- `name`: a unique name for the job. - `name`: a unique name for the job.
- `schedule`: a [cron expression](https://crontab.guru/). - `schedule`: a [cron expression](https://crontab.guru/) specifying when to run the job.
This scheduled job executes every minute and logs into the terminal `Time to say hello world!`. This scheduled job executes every minute and logs into the terminal `Time to say hello world!`.
@@ -81,13 +81,13 @@ Time to say hello world!
## Resolve Resources ## 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. The scheduled job function receives a `container` parameter, which is the Medusa container. Use it to resolve resources in your Medusa application, such as services.
For example: For example:
export const highlights = [ export const highlights = [
["11", "resolve", "Resolve the Product Module's main service."], ["11", "resolve", "Resolve the Product Module's main service."],
["11", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."] ["11", "ModuleRegistrationName.PRODUCT", "The module's registration name imported from `@medusajs/modules-sdk`."]
] ]
```ts title="src/jobs/hello-world.ts" highlights={highlights} ```ts title="src/jobs/hello-world.ts" highlights={highlights}
@@ -111,7 +111,7 @@ export default async function myCustomJob(
} }
export const config = { export const config = {
name: "hello-world", name: "every-minute-message",
// execute every minute // execute every minute
schedule: "* * * * *", schedule: "* * * * *",
} }
+27 -16
View File
@@ -12,11 +12,11 @@ In this chapter, you’ll learn about workflows and how to define and execute th
A workflow is a series of queries and actions that complete a task. 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 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 roll back steps. You construct a workflow similar to how you create a JavaScript function, but unlike regular functions, a 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 roll back steps.
--- ---
## How to Create and Execute a Workflow ## How to Create and Execute a Workflow?
### 1. Create the Steps ### 1. Create the Steps
@@ -34,7 +34,9 @@ const step1 = createStep("step-1", async () => {
This creates one step that returns a hello message. This creates one step that returns a hello message.
Steps can accepts input parameters. For example: Steps can accept input parameters.
For example, add the following to `src/workflows/hello-world.ts`:
```ts title="src/workflows/hello-world.ts" ```ts title="src/workflows/hello-world.ts"
type WorkflowInput = { type WorkflowInput = {
@@ -48,7 +50,7 @@ const step2 = createStep("step-2", async ({ name }: WorkflowInput) => {
### 2. Create a Workflow ### 2. Create a Workflow
Next, create the workflow using the `createWorkflow` function: Next, add the following to the same file to create the workflow using the `createWorkflow` function:
```ts title="src/workflows/hello-world.ts" ```ts title="src/workflows/hello-world.ts"
import { import {
@@ -93,7 +95,7 @@ To execute the workflow, invoke it passing the Medusa container as a parameter.
<CodeTabs group="resource-types"> <CodeTabs group="resource-types">
<CodeTab label="API Route" value="api-route"> <CodeTab label="API Route" value="api-route">
```ts title="src/api/store/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} ```ts title="src/api/store/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type { import type {
MedusaRequest, MedusaRequest,
MedusaResponse, MedusaResponse,
@@ -118,23 +120,30 @@ To execute the workflow, invoke it passing the Medusa container as a parameter.
</CodeTab> </CodeTab>
<CodeTab label="Subscriber" value="subscriber"> <CodeTab label="Subscriber" value="subscriber">
```ts title="src/subscribers/customer-created.ts" highlights={[["13"], ["14"], ["15"], ["16"], ["17"], ["18"]]} ```ts title="src/subscribers/customer-created.ts" highlights={[["20"], ["21"], ["22"], ["23"], ["24"], ["25"]]} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import { import {
type SubscriberConfig, type SubscriberConfig,
type SubscriberArgs, type SubscriberArgs,
CustomerService,
Customer,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import myWorkflow from "../workflows/hello-world" import myWorkflow from "../workflows/hello-world"
import { ModuleRegistrationName } from "@medusajs/utils"
import { IUserModuleService } from "@medusajs/types"
export default async function handleCustomerCreate({ export default async function handleCustomerCreate({
data, data,
container, container,
}: SubscriberArgs<Customer>) { }: SubscriberArgs<{ id: string }>) {
const userId = "data" in data ? data.data.id : data.id
const userModuleService: IUserModuleService = container.resolve(
ModuleRegistrationName.USER
)
const user = await userModuleService.retrieveUser(userId)
const { result } = await myWorkflow(container) const { result } = await myWorkflow(container)
.run({ .run({
input: { input: {
name: data.first_name, name: user.first_name,
}, },
}) })
@@ -142,7 +151,7 @@ To execute the workflow, invoke it passing the Medusa container as a parameter.
} }
export const config: SubscriberConfig = { export const config: SubscriberConfig = {
event: CustomerService.Events.CREATED, event: "user.created",
} }
``` ```
@@ -205,7 +214,7 @@ You’ll receive the following response:
- You're defining a flow with interactions across multiple systems and services. - 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'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. - You want to define configurations related to errors and how to roll-back steps. This is explained more in later chapters.
</Note> </Note>
@@ -240,10 +249,12 @@ type WorkflowOutput = {
} }
const step1 = createStep("step-1", async (_, context) => { const step1 = createStep("step-1", async (_, context) => {
const productModuleService: IProductModuleService = context.container.resolve( const productModuleService: IProductModuleService =
ModuleRegistrationName.PRODUCT context.container.resolve(ModuleRegistrationName.PRODUCT)
)
return new StepResponse((await productModuleService.listAndCount())[1]) const [, count] = await productModuleService.listAndCountProducts()
return new StepResponse(count)
}) })
const myWorkflow = createWorkflow<unknown, WorkflowOutput>( const myWorkflow = createWorkflow<unknown, WorkflowOutput>(
@@ -1,88 +0,0 @@
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 configurations 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 = defineConfig({
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>
@@ -8,7 +8,7 @@ In this chapter, you’ll learn how to use Medusa’s logging utility.
## Logger Class ## Logger Class
Medusa provides a `Logger` class that provides advanced logging functionalities. This includes configuring logging levels or saving logs to a file. Medusa provides a `Logger` class with advanced logging functionalities. This includes configuring logging levels or saving logs to a file.
The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`. The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`.
@@ -18,34 +18,43 @@ The Medusa application registers the `Logger` class in the Medusa container and
Resolve the `logger` using the Medusa container to log a message in your resource. 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: For example, create the file `src/jobs/log-message.ts` with the following content:
export const highlights = [ export const highlights = [
["4", "resolve", "Resolve the `Logger` class."], ["7", "resolve", "Resolve the `Logger` class."],
["6", "info", "Log a message of level `info`."] ["9", "info", "Log a message of level `info`."]
] ]
```ts title="src/loaders/log-message.ts" highlights={highlights} ```ts title="src/jobs/log-message.ts" highlights={highlights}
import { Logger, MedusaContainer } from "@medusajs/medusa" import { Logger } from "@medusajs/medusa"
import { MedusaContainer } from "@medusajs/types"
export default async (container: MedusaContainer) => { export default async function myCustomJob(
container: MedusaContainer
) {
const logger: Logger = container.resolve("logger") const logger: Logger = container.resolve("logger")
logger.info("I'm using the logger!") logger.info("I'm using the logger!")
} }
export const config = {
name: "test-logger",
// execute every minute
schedule: "* * * * *",
}
``` ```
This creates a loader that resolves the `logger` from the Medusa container and uses it to log a message. This creates a scheduled job that resolves the `logger` from the Medusa container and uses it to log a message.
### Test the Loader ### Test the Scheduled Job
To test out the above loader, start the Medusa application: To test out the above scheduled job, start the Medusa application:
```bash npm2yarn ```bash npm2yarn
npm run dev npm run dev
``` ```
You’ll see the following message as part of the logged messages: After a minute, you'll see the following message as part of the logged messages:
```text ```text
info: I'm using the logger! info: I'm using the logger!
@@ -116,7 +125,7 @@ The environment variable must be set as a system environment variable and not in
## Show Log with Progress ## 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. The `Logger` class has an `activity` method used to log a message of level `info`. If the Medusa application is running in a development environment, a spinner starts to show the activity's progress.
For example: For example:
@@ -16,46 +16,39 @@ Since the Medusa server is a Node.js server, you can use any Node.js testing too
[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. [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.
<Note>
Refer to [Jest's documentation](https://jestjs.io/docs/getting-started) to learn how to install and configure it.
</Note>
For example, consider the following service created at `src/modules/hello/service.ts`: For example, consider the following service created at `src/modules/hello/service.ts`:
```ts title="src/modules/hello/service.ts" ```ts title="src/modules/hello/service.ts"
import { TransactionBaseService } from "@medusajs/medusa" class HelloModuleService {
class HelloWorldService extends TransactionBaseService {
constructor(container) {
super(arguments[0])
}
getMessage(): string { getMessage(): string {
return "Hello, world!" return "Hello, world!"
} }
} }
export default HelloWorldService export default HelloModuleService
``` ```
You can write a test for it in the file `src/modules/hello/__tests__/hello-world.ts`: You can write a test for it in the file `src/modules/hello/__tests__/hello-world.spec.ts`:
```ts title="src/modules/hello/__tests__/hello-world.ts" ```ts title="src/modules/hello/__tests__/hello-world.spec.ts"
import HelloWorldService from "../hello-world" import HelloModuleService from "../service"
describe("HelloWorldService", () => { describe("HelloModuleService", () => {
const helloWorldService = new HelloWorldService() const helloModuleService = new HelloModuleService()
it("should return hello world message", () => { it("should return hello world message", () => {
expect(helloWorldService.getMessage()).toBe("Hello, world!") expect(helloModuleService.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 ## IDE Debugging Tools
+3 -20
View File
@@ -25,7 +25,7 @@ You must deploy the Medusa server before the admin or storefront, as both of the
![Diagram showcasing how the Medusa server and its associated services would be deployed](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600972/Medusa%20Book/backend_deployment_pgexo3.jpg) ![Diagram showcasing how the Medusa server and its associated services would be deployed](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600972/Medusa%20Book/backend_deployment_pgexo3.jpg)
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… The Medusa server must be deployed to a hosting provider supporting Node.js server deployments, such as Railway, DigitalOcean, AWS, Heruko, etc…
<Note title="Tip"> <Note title="Tip">
@@ -33,7 +33,7 @@ For optimal experience, make sure that the hosting provider and plan offer at le
</Note> </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. Your server connects to a PostgreSQL database and, optionally, Redis or other services. Most hosting providers support deploying and managing these databases along with your Medusa server (such as Railway and DigitalOcean).
Refer to [this reference](!resources!/deployment) to find how-to deployment guides for specific hosting providers. Refer to [this reference](!resources!/deployment) to find how-to deployment guides for specific hosting providers.
@@ -41,15 +41,9 @@ Refer to [this reference](!resources!/deployment) to find how-to deployment guid
## Deploying the Medusa Admin ## Deploying the Medusa Admin
<Note type="soon" title="Development Notice">
Admin deployment details are still in the work.
</Note>
### Deploy Admin with the Server ### Deploy Admin with the Server
Since the Medusa Admin is a plugin installed in the server, you may choose to host them together. The Medusa Admin can be deployed on the same hosting with the server.
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. 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.
@@ -61,17 +55,6 @@ The server deployment guides mention details on how to deploy the admin along wi
</Note> </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.
![Diagram showcasing how the admin can be built through a GitHub action before deployment](https://res.cloudinary.com/dza7lstvk/image/upload/v1708601239/Medusa%20Book/admin_deployment_1_-_actions_cxjv6h.jpg)
### Deploy Admin Separately ### 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. 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.
+2 -6
View File
@@ -4,10 +4,6 @@ export const metadata = {
# {metadata.title} # {metadata.title}
In this chapter, you’ll find more resources to aid you during your development with Medusa. The Learning Resources documentation provides guides and references that are useful for your development. This documentation included links to parts of the Learning Resources documentation where necessary.
## Medusa Resources Documentation Check out the Learning Resources documentation [here](!resources!).
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.
Check out the Medusa Resources documentation [here](!resources!).
+4 -4
View File
@@ -12,7 +12,7 @@ Medusa is a digital commerce platform with a built-in framework for customizatio
Medusa's framework includes two types of tools: Medusa's framework includes two types of tools:
1. Modules that provide commerce functionalities, such as Product, Order, and Cart modules. 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. 2. Customization tools to build your business features, such as custom data models, business logic, flows, and 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. 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.
@@ -23,7 +23,7 @@ With these tools, you save time you would spend with other platforms on maintain
<Note type="check"> <Note type="check">
- [Node.js v16+](https://nodejs.org/en/download) - [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads) - [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/) - [PostgreSQL](https://www.postgresql.org/download/)
@@ -32,7 +32,7 @@ With these tools, you save time you would spend with other platforms on maintain
Create your first Medusa store by running the following command: Create your first Medusa store by running the following command:
```bash ```bash
npx create-medusa-app@latest --v2 npx create-medusa-app@preview
``` ```
--- ---
@@ -96,4 +96,4 @@ Medusa is for ambitious businesses and developers that are limited by traditiona
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. 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 an expert Medusa developer, leading teams using Medusa from the beginning till deployment. By the end of this book, you’ll be an expert Medusa developer, leading teams using Medusa from the development till production.
@@ -8,7 +8,7 @@ In this chapter, you’ll learn how to install and use the Next.js Starter store
<Note type="soon"> <Note type="soon">
The Next.js starter is currently in development to fully support Medusa v2. The Next.js Starter is currently in development to fully support Medusa v2.
</Note> </Note>
@@ -21,10 +21,10 @@ The Next.js starter is currently in development to fully support Medusa v2.
</Note> </Note>
1. Clone the `feat/v2` branch of the [Next.js Starter](https://github.com/medusajs/nextjs-starter-medusa): 1. Clone the `v2` branch of the [Next.js Starter](https://github.com/medusajs/nextjs-starter-medusa/tree/v2):
```bash ```bash
git clone https://github.com/medusajs/nextjs-starter-medusa -b feat/v2 my-medusa-storefront git clone https://github.com/medusajs/nextjs-starter-medusa -b v2 my-medusa-storefront
``` ```
2. Change to the `my-medusa-storefront` directory, install the dependencies, and rename the template environment variable: 2. Change to the `my-medusa-storefront` directory, install the dependencies, and rename the template environment variable:
@@ -61,8 +61,8 @@ You can learn more about development with Next.js through [their documentation](
--- ---
## Configurations and Compatible Plugins ## Configurations and Integrations
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. The Next.js Starter is compatible with some Medusa integrations out-of-the-box, such as the Stripe provider module. You can also change some of its configurations if necessary.
Refer to the Next.js Starter reference for more details. Refer to the [Next.js Starter reference](!resources!/nextjs-starter) for more details.
@@ -4,13 +4,9 @@ export const metadata = {
# {metadata.title} # {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. Storefronts are built separately from the Medusa application. They interact with the Medusa application through the Store API routes.
You're free to choose how 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. You're free to choose how to build your storefront. You can start with our Next.js starter storefront, as explained in the next chapter, or build a storefront from scratch using your preferred framework or tech stack.
<Note title="Tip"> <Note title="Tip">
+1 -1
View File
@@ -42,6 +42,6 @@
"typescript": "^5" "typescript": "^5"
}, },
"engines": { "engines": {
"node": ">=18.17.0" "node": ">=20"
} }
} }
+14 -26
View File
@@ -17,8 +17,8 @@ export const sidebar = sidebarAttachHrefCommonOptions(
title: "The Basics", title: "The Basics",
children: [ children: [
{ {
path: "/basics/important-directories-files", path: "/basics/project-directories-files",
title: "Important Directories and Files", title: "Project Directories and Files",
}, },
{ {
path: "/basics/medusa-container", path: "/basics/medusa-container",
@@ -85,14 +85,14 @@ export const sidebar = sidebarAttachHrefCommonOptions(
path: "/advanced-development/api-routes/middlewares", path: "/advanced-development/api-routes/middlewares",
title: "Middlewares", title: "Middlewares",
}, },
{
path: "/advanced-development/api-routes/cors",
title: "Handling CORS",
},
{ {
path: "/advanced-development/api-routes/protected-routes", path: "/advanced-development/api-routes/protected-routes",
title: "Protected Routes", title: "Protected Routes",
}, },
{
path: "/advanced-development/api-routes/cors",
title: "Handling CORS",
},
], ],
}, },
{ {
@@ -111,17 +111,17 @@ export const sidebar = sidebarAttachHrefCommonOptions(
title: "Module Isolation", title: "Module Isolation",
}, },
{ {
path: "/advanced-development/modules/remote-query", path: "/advanced-development/modules/module-links",
title: "Remote Query", title: "Module Links",
},
{
path: "/advanced-development/modules/link-modules",
title: "Link Modules",
}, },
{ {
path: "/advanced-development/modules/remote-link", path: "/advanced-development/modules/remote-link",
title: "Remote Link", title: "Remote Link",
}, },
{
path: "/advanced-development/modules/remote-query",
title: "Remote Query",
},
{ {
path: "/advanced-development/modules/options", path: "/advanced-development/modules/options",
title: "Module Options", title: "Module Options",
@@ -129,6 +129,7 @@ export const sidebar = sidebarAttachHrefCommonOptions(
], ],
}, },
{ {
path: "/advanced-development/data-models",
title: "Data Models", title: "Data Models",
children: [ children: [
{ {
@@ -161,15 +162,6 @@ export const sidebar = sidebarAttachHrefCommonOptions(
}, },
], ],
}, },
{
title: "Loaders",
children: [
{
path: "/advanced-development/loaders/outside-modules",
title: "Create Outside Module",
},
],
},
{ {
title: "Events and Subscribers", title: "Events and Subscribers",
children: [ children: [
@@ -260,7 +252,7 @@ export const sidebar = sidebarAttachHrefCommonOptions(
], ],
}, },
{ {
path: "/architectural-concepts/architectural-modules", path: "/architectural-modules",
title: "Architectural Modules", title: "Architectural Modules",
}, },
{ {
@@ -275,10 +267,6 @@ export const sidebar = sidebarAttachHrefCommonOptions(
path: "/debugging-and-testing/tools", path: "/debugging-and-testing/tools",
title: "Tools", title: "Tools",
}, },
{
path: "/debugging-and-testing/feature-flags",
title: "Feature Flags",
},
], ],
}, },
{ {
@@ -135,10 +135,11 @@ Create the file `src/modules/my-cache/index.ts` with the following content:
```ts title="src/modules/my-cache/index.ts" ```ts title="src/modules/my-cache/index.ts"
import MyCacheService from "./service" import MyCacheService from "./service"
import { Module } from "@medusajs/utils"
export default { export default Module("my-cache", {
service: MyCacheService, service: MyCacheService,
} })
``` ```
This exports the module's definition, indicating that the `MyCacheService` is the main service of the module. This exports the module's definition, indicating that the `MyCacheService` is the main service of the module.
@@ -152,7 +153,7 @@ To use your Cache Module, add it to the `modules` object exported as part of the
For example: For example:
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -31,7 +31,7 @@ npm install @medusajs/cache-inmemory
Next, add the module into the `modules` property of the exported object in `medusa-config.js`: Next, add the module into the `modules` property of the exported object in `medusa-config.js`:
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -31,7 +31,7 @@ export const highlights = [
] ]
```js title="medusa-config.js" highlights={highlights} ```js title="medusa-config.js" highlights={highlights}
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -88,12 +88,13 @@ class MyEventService extends AbstractEventBusModuleService {
Create the file `src/modules/my-event/index.ts` with the following content: Create the file `src/modules/my-event/index.ts` with the following content:
```ts ```ts title="src/modules/my-event/index.ts"
import MyEventService from "./service" import MyEventService from "./service"
import { Module } from "@medusajs/utils"
export default { export default Module("my-event", {
service: MyEventService, service: MyEventService,
} })
``` ```
This exports the module's definition, indicating that the `MyEventService` is the main service of the module. This exports the module's definition, indicating that the `MyEventService` is the main service of the module.
@@ -107,7 +108,7 @@ To use your Event Module, add it to the `modules` object exported as part of the
For example: For example:
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -29,7 +29,7 @@ npm install @medusajs/event-local
Next, add the module into the `modules` property of the exported object in `medusa-config.js`: Next, add the module into the `modules` property of the exported object in `medusa-config.js`:
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -35,7 +35,7 @@ export const highlights = [
] ]
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -33,7 +33,7 @@ The File Module accepts one provider only.
</Note> </Note>
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -46,14 +46,11 @@ module.exports = {
providers: [ providers: [
{ {
resolve: "@medusajs/file-local-next", resolve: "@medusajs/file-local-next",
id: "local",
options: { options: {
config: {
local: {
// provider options... // provider options...
}, },
}, },
},
},
], ],
}, },
}, },
@@ -85,7 +85,7 @@ The File Module accepts one provider only.
</Note> </Note>
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -99,9 +99,8 @@ module.exports = {
providers: [ providers: [
{ {
resolve: "@medusajs/file-s3", resolve: "@medusajs/file-s3",
id: "s3",
options: { options: {
config: {
s3: {
file_url: process.env.S3_FILE_URL, file_url: process.env.S3_FILE_URL,
access_key_id: process.env.S3_ACCESS_KEY_ID, access_key_id: process.env.S3_ACCESS_KEY_ID,
secret_access_key: process.env.S3_SECRET_ACCESS_KEY, secret_access_key: process.env.S3_SECRET_ACCESS_KEY,
@@ -111,8 +110,6 @@ module.exports = {
// other options... // other options...
}, },
}, },
},
},
], ],
}, },
}, },
@@ -33,7 +33,7 @@ Only one provider can be defined for a channel.
</Note> </Note>
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -47,14 +47,11 @@ module.exports = defineConfig({
// ... // ...
{ {
resolve: "@medusajs/notification-local", resolve: "@medusajs/notification-local",
id: "local",
options: { options: {
config: {
local: {
channels: ["email"], channels: ["email"],
}, },
}, },
},
},
], ],
}, },
}, },
@@ -31,7 +31,7 @@ When you send a notification, you specify the channel to send it through, such a
For example: For example:
```js title="medusa-config.js" highlights={[["19"]]} ```js title="medusa-config.js" highlights={[["19"]]}
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -46,14 +46,11 @@ module.exports = {
// ... // ...
{ {
resolve: "@medusajs/notification-local", resolve: "@medusajs/notification-local",
id: "notification",
options: { options: {
config: {
local: {
channels: ["email"], channels: ["email"],
}, },
}, },
},
},
], ],
}, },
}, },
@@ -33,7 +33,10 @@ export const highlights = [
] ]
```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" ```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import type { SubscriberArgs, SubscriberConfig } from "@medusajs/medusa" import type {
SubscriberArgs,
SubscriberConfig,
} from "@medusajs/medusa"
import { ModuleRegistrationName } from "@medusajs/utils" import { ModuleRegistrationName } from "@medusajs/utils"
import { INotificationModuleService } from "@medusajs/types" import { INotificationModuleService } from "@medusajs/types"
@@ -35,7 +35,7 @@ Only one provider can be defined for a channel.
</Note> </Note>
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -49,16 +49,13 @@ module.exports = defineConfig({
// ... // ...
{ {
resolve: "@medusajs/notification-sendgrid", resolve: "@medusajs/notification-sendgrid",
id: "sendgrid",
options: { options: {
config: {
sendgrid: {
channels: ["email"], channels: ["email"],
api_key: process.env.SENDGRID_API_KEY, api_key: process.env.SENDGRID_API_KEY,
from: process.env.SENDGRID_FROM, from: process.env.SENDGRID_FROM,
}, },
}, },
},
},
], ],
}, },
}, },
@@ -130,7 +127,10 @@ export const highlights = [
] ]
```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" ```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import type { SubscriberArgs, SubscriberConfig } from "@medusajs/medusa" import type {
SubscriberArgs,
SubscriberConfig,
} from "@medusajs/medusa"
import { ModuleRegistrationName } from "@medusajs/utils" import { ModuleRegistrationName } from "@medusajs/utils"
import { INotificationModuleService } from "@medusajs/types" import { INotificationModuleService } from "@medusajs/types"
@@ -31,7 +31,7 @@ npm install @medusajs/workflow-engine-inmemory
Next, add the module into the `modules` property of the exported object in `medusa-config.js`: Next, add the module into the `modules` property of the exported object in `medusa-config.js`:
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -31,7 +31,7 @@ export const highlights = [
] ]
```js title="medusa-config.js" highlights={highlights} ```js title="medusa-config.js" highlights={highlights}
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -42,7 +42,7 @@ module.exports = defineConfig({
resolve: "@medusajs/workflow-engine-redis", resolve: "@medusajs/workflow-engine-redis",
options: { options: {
redis: { redis: {
url: process.emv.WE_REDIS_URL, url: process.env.WE_REDIS_URL,
}, },
}, },
}, },
@@ -230,7 +230,10 @@ export async function POST(request: Request, { params }: ContextType) {
<CodeTab label="Medusa API Router" value="medusa"> <CodeTab label="Medusa API Router" value="medusa">
```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" ```ts collapsibleLines="1-8" expandButtonLabel="Show Imports"
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/medusa" import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import { IApiKeyModuleService } from "@medusajs/types" import { IApiKeyModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/utils" import { ModuleRegistrationName } from "@medusajs/utils"
@@ -37,7 +37,7 @@ npm install @medusajs/auth-google
Next, add the module to the array of providers passed to the Auth Module: Next, add the module to the array of providers passed to the Auth Module:
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -49,9 +49,8 @@ const modules = {
providers: [ providers: [
{ {
resolve: "@medusajs/auth-google", resolve: "@medusajs/auth-google",
id: "google",
options: { options: {
config: {
google: {
clientID: process.env.GOOGLE_CLIENT_ID, clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET, clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL, callbackURL: process.env.GOOGLE_CALLBACK_URL,
@@ -59,8 +58,6 @@ const modules = {
process.env.GOOGLE_SUCCESS_REDIRECT_URL, process.env.GOOGLE_SUCCESS_REDIRECT_URL,
}, },
}, },
},
},
], ],
}, },
}, },
@@ -21,7 +21,7 @@ The Emailpass auth provider is registered by default with the Auth Module.
If you want to pass options to the provider, add the provider to the `providers` option of the Auth Module: If you want to pass options to the provider, add the provider to the `providers` option of the Auth Module:
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -33,12 +33,11 @@ const modules = {
providers: [ providers: [
{ {
resolve: "@medusajs/auth-emailpass", resolve: "@medusajs/auth-emailpass",
config: { id: "emailpass",
emailpass: { options: {
// options... // options...
}, },
}, },
},
], ],
}, },
}, },
@@ -21,7 +21,10 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j
```ts collapsibleLines="1-10" expandButtonLabel="Show Imports" ```ts collapsibleLines="1-10" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa" import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
import { IAuthModuleService, AuthenticationInput } from "@medusajs/types" import {
IAuthModuleService,
AuthenticationInput,
} from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/utils" import { ModuleRegistrationName } from "@medusajs/utils"
import { MedusaError } from "@medusajs/utils" import { MedusaError } from "@medusajs/utils"
import jwt from "jsonwebtoken" import jwt from "jsonwebtoken"
@@ -122,7 +125,10 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j
```ts collapsibleLines="1-10" expandButtonLabel="Show Imports" ```ts collapsibleLines="1-10" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa" import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
import { IAuthModuleService, AuthenticationInput } from "@medusajs/types" import {
IAuthModuleService,
AuthenticationInput,
} from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/utils" import { ModuleRegistrationName } from "@medusajs/utils"
import { MedusaError } from "@medusajs/utils" import { MedusaError } from "@medusajs/utils"
import jwt from "jsonwebtoken" import jwt from "jsonwebtoken"
@@ -21,7 +21,7 @@ When the Medusa application starts, these providers are registered and can be us
For example: For example:
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -32,15 +32,12 @@ module.exports = defineConfig({
options: { options: {
providers: [ providers: [
{ {
resolve: "@medusajs/auth-google", resolve: "@medusajs/auth-emailpass",
id: "emailpass",
options: { options: {
config: {
google: {
// provider options... // provider options...
}, },
}, },
},
},
], ],
}, },
}, },
@@ -50,8 +47,8 @@ module.exports = defineConfig({
The `providers` option is an array of objects that accept the following properties: The `providers` option is an array of objects that accept the following properties:
- `resolve`: A string indicating the package name of the module provider or the path to it. - `resolve`: A string indicating the package name of the module provider or the path to it.
- `options`: An optional object of the module provider's options. The object must have the following property: - `id`: A string indicating the provider's unique name or ID.
- `config`: An object whose key is the ID of the auth provider, and its value is an object of options to pass to the module provider. - `options`: An optional object of the module provider's options.
--- ---
@@ -21,7 +21,7 @@ When the Medusa application starts, these providers are registered and can be us
For example: For example:
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -34,14 +34,11 @@ module.exports = defineConfig({
providers: [ providers: [
{ {
resolve: `@medusajs/fulfillment-manual`, resolve: `@medusajs/fulfillment-manual`,
name: "manual",
options: { options: {
config: {
manual: {
// provider options... // provider options...
}, },
}, },
},
},
], ],
}, },
}, },
@@ -52,5 +49,5 @@ module.exports = defineConfig({
The `providers` option is an array of objects that accept the following properties: The `providers` option is an array of objects that accept the following properties:
- `resolve`: A string indicating either the package name of the module provider or the path to it relative to the `src` directory. - `resolve`: A string indicating either the package name of the module provider or the path to it relative to the `src` directory.
- `options`: An optional object of the module provider's options. The object must have the following property: - `name`: A string indicating the provider's unique name.
- `config`: An object whose key is the ID of the module provider, and its value is an object of options to pass to the module provider. - `options`: An optional object of the module provider's options.
@@ -104,7 +104,7 @@ When the Medusa application starts, these providers are registered and can be us
For example: For example:
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -117,6 +117,7 @@ module.exports = defineConfig({
providers: [ providers: [
{ {
resolve: "@medusajs/payment-stripe", resolve: "@medusajs/payment-stripe",
id: "stripe",
options: { options: {
// ... // ...
}, },
@@ -131,5 +132,5 @@ module.exports = defineConfig({
The `providers` option is an array of objects that accept the following properties: The `providers` option is an array of objects that accept the following properties:
- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory. - `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory.
- `options`: An optional object of the module provider's options. The object must have the following property: - `id`: A string indicating the provider's unique name or ID.
- `config`: An object whose key is the ID of the module provider, and its value is an object of options to pass to the module provider. - `options`: An optional object of the module provider's options.
@@ -25,7 +25,7 @@ These features are also available in a safe test environment, allowing for a con
<Note type="check"> <Note type="check">
- [Stripe account](https://stripe.com/). - [Stripe account](https://stripe.com/).
- [Stripe API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) - [Stripe Secret API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard)
- For deployed Medusa applications, a [Stripe webhook secret](https://docs.stripe.com/webhooks#add-a-webhook-endpoint). When creating the Webhook, set the endpoint URL to `{medusa_url}/hooks/payment/stripe`, where `{medusa_url}` with the URL to your deployed Medusa application. - For deployed Medusa applications, a [Stripe webhook secret](https://docs.stripe.com/webhooks#add-a-webhook-endpoint). When creating the Webhook, set the endpoint URL to `{medusa_url}/hooks/payment/stripe`, where `{medusa_url}` with the URL to your deployed Medusa application.
</Note> </Note>
@@ -39,7 +39,7 @@ npm install @medusajs/payment-stripe
Next, add the module to the array of providers passed to the Payment Module: Next, add the module to the array of providers passed to the Payment Module:
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -52,16 +52,9 @@ module.exports = defineConfig({
providers: [ providers: [
{ {
resolve: "@medusajs/payment-stripe", resolve: "@medusajs/payment-stripe",
id: "stripe",
options: { options: {
config: { apiKey: process.env.STRIPE_API_KEY,
stripe: {
credentials: {
usd: {
apiKey: process.env.STRIPE_USD_API_KEY,
},
},
},
},
}, },
}, },
], ],
@@ -76,7 +69,7 @@ module.exports = defineConfig({
Make sure to add the necessary environment variables for the above options in `.env`: Make sure to add the necessary environment variables for the above options in `.env`:
```bash ```bash
STRIPE_USD_API_KEY=<YOUR_STRIPE_API_KEY> STRIPE_API_KEY=<YOUR_STRIPE_API_KEY>
``` ```
### Module Options ### Module Options
@@ -94,17 +87,12 @@ STRIPE_USD_API_KEY=<YOUR_STRIPE_API_KEY>
<Table.Row> <Table.Row>
<Table.Cell> <Table.Cell>
`credentials` `apiKey`
</Table.Cell> </Table.Cell>
<Table.Cell> <Table.Cell>
An object where each entry is a stripe provider installation. The object’s keys are the name suffix of the provider, where the provider name will be formatted as `stripe-{key}`. For example, `stripe-usd`. A string indicating the Stripe Secret API key.
Each value is an object that accepts the following properties:
- `apiKey`: A string indicating the Stripe API key.
- `webhookSecret`: (optional in development) A string indicating the Stripe webhook secret. This is only useful for deployed Medusa applications.
</Table.Cell> </Table.Cell>
<Table.Cell> <Table.Cell>
@@ -114,7 +102,29 @@ STRIPE_USD_API_KEY=<YOUR_STRIPE_API_KEY>
</Table.Cell> </Table.Cell>
<Table.Cell> <Table.Cell>
\- -
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`webhookSecret`
</Table.Cell>
<Table.Cell>
A string indicating the Stripe webhook secret. This is only useful for deployed Medusa applications.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
-
</Table.Cell> </Table.Cell>
</Table.Row> </Table.Row>
@@ -19,7 +19,7 @@ The `providers` option is an array of either tax module providers, tax plugins,
When the Medusa application starts, these providers are registered and can be used to retrieve tax lines. When the Medusa application starts, these providers are registered and can be used to retrieve tax lines.
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -31,7 +31,8 @@ module.exports = defineConfig({
options: { options: {
providers: [ providers: [
{ {
resolve: "my-provider", resolve: "./path/to/my-provider",
id: "my-provider",
options: { options: {
// ... // ...
}, },
@@ -46,4 +47,5 @@ module.exports = defineConfig({
The objects in the array accept the following properties: The objects in the array accept the following properties:
- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory. - `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory.
- `options`: An object of options to pass to the module provider. - `id`: A string indicating the provider's unique name or ID.
- `options`: An optional object of the module provider's options.
@@ -15,7 +15,7 @@ In this document, you'll learn about the options of the User Module.
## Module Options ## Module Options
```js title="medusa-config.js" ```js title="medusa-config.js"
const { Modules } = require("@medusajs/modules-sdk") import { Modules } from "@medusajs/utils"
// ... // ...
@@ -90,7 +90,6 @@ export const serviceHighlights = [
} }
export default ErpModuleService export default ErpModuleService
``` ```
This creates the module's main service. Few things to note: This creates the module's main service. Few things to note:
@@ -111,11 +110,12 @@ export const serviceHighlights = [
```ts title="src/modules/erp/index.ts" ```ts title="src/modules/erp/index.ts"
import ErpModuleService from "./service" import ErpModuleService from "./service"
import { Module } from "@medusajs/utils"
export default { export default Module("erp", {
service: ErpModuleService, service: ErpModuleService,
} })
```` ```
Finally, add the module to the `modules` object in `medusa-config.js`: Finally, add the module to the `modules` object in `medusa-config.js`:
@@ -168,28 +168,12 @@ export const workflowHighlights = [
["24", "productModuleService", "Resolve the Product Module's main service."], ["24", "productModuleService", "Resolve the Product Module's main service."],
["28", "retrieve", "Retrieve the created product's data."], ["28", "retrieve", "Retrieve the created product's data."],
["30", "createProduct", "Create the product in the ERP system."], ["30", "createProduct", "Create the product in the ERP system."],
[ ["34", "update", "Update the product in Medusa with the ID of the ERP product."],
"34",
"update",
"Update the product in Medusa with the ID of the ERP product.",
],
["43", "erpId", "Pass the ERP product's ID to the compensation function."], ["43", "erpId", "Pass the ERP product's ID to the compensation function."],
["44", "productId", "Pass the product's ID to the compensation function."], ["44", "productId", "Pass the product's ID to the compensation function."],
[ ["46", "", "Define a compensation function that rolls back changes when an error occurs."],
"46", ["53", "deleteProduct", "Undo creating the product in the ERP system by deleting it."],
"", ["54", "update", "Update the product in Medusa to remove the ERP product's ID."]
"Define a compensation function that rolls back changes when an error occurs.",
],
[
"53",
"deleteProduct",
"Undo creating the product in the ERP system by deleting it.",
],
[
"54",
"update",
"Update the product in Medusa to remove the ERP product's ID.",
],
] ]
```ts title="src/workflows/create-product.ts" highlights={workflowHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" ```ts title="src/workflows/create-product.ts" highlights={workflowHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
+4 -1
View File
@@ -76,7 +76,10 @@ To search through product variants by their barcode, create a custom API Route a
Here’s an example of creating a custom API Route at `/store/pos/search-barcode` that searches product variants by a barcode: Here’s an example of creating a custom API Route at `/store/pos/search-barcode` that searches product variants by a barcode:
```ts title="src/api/store/pos/search-barcode/route.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports" ```ts title="src/api/store/pos/search-barcode/route.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports"
import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa" import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import { MedusaError } from "@medusajs/utils" import { MedusaError } from "@medusajs/utils"
import { ModuleRegistrationName } from "@medusajs/utils" import { ModuleRegistrationName } from "@medusajs/utils"
import { IProductModuleService } from "@medusajs/types" import { IProductModuleService } from "@medusajs/types"
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff. Show more