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
2024-07-04 17:26:03 +03:00
committed by GitHub
parent 32982e708a
commit 964927b597
149 changed files with 1676 additions and 3008 deletions
@@ -1,82 +0,0 @@
export const metadata = {
title: `${pageNumber} Create a Batch Job`,
}
# {metadata.title}
In this chapter, youll 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 jobs 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 its completed, the status is `completed`.
## 4. Check Console
Once the batch jobs status is `completed`, check the terminal. Youll 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 jobs lifecycle. Whenever the batch jobs status is changed, an event is triggered to allow batch job strategies to handle them.
When the batch jobs status is `confirmed`, the batch job is processed, which is why the `Processing the batch job!` message is logged.
In the next chapter, youll learn more about the batch jobs 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, youll 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 jobs 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 jobs status is changed to `pre_processed`, its confirmed automatically if the jobs `dry_run` attribute is disabled (which is, by default). The batch jobs 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 jobs status is changed to `processing` and the `batch.processing` event is emitted. Then, the batch job strategys `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 jobs status is changed to `canceled` and the `batch.canceled` event is emitted.
- `failed`: If an error occurs during any stage, the batch jobs 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, youll 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 applications 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, youll 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 doesnt 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">
- Youre implementing an asynchronous job thats 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="Dont 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 Repositorys 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 [Typeorms 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, youll 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 managers `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 transactions entity manager as a parameter. When retrieving a data models 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 youre 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, youll 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 methods 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 methods 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
To navigate or link to other pages, or use other routing functionalities, use the [react-router-dom](https://reactrouter.com/en/main) package:
```bash npm2yarn
npm install react-router-dom
```
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.
For example:
@@ -37,7 +33,7 @@ const ProductWidget = () => {
// The widget's configurations
export const config = defineWidgetConfig({
zone: "product.details.after",
zone: "product.details.before",
})
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 files default export must be the UI routes React component.
For example, create the file `src/admin/routes/custom/page.tsx` with the following content:
```tsx title="src/admin/routes/custom/page.tsx"
import { Container } from "@medusajs/ui"
const CustomPage = () => {
return <div>This is my custom route</div>
return <Container>This is my custom route</Container>
}
export default CustomPage
@@ -30,7 +32,9 @@ export default CustomPage
The new pages path is the files 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:
@@ -56,37 +60,6 @@ export const highlights = [
```tsx title="src/admin/routes/custom/page.tsx" highlights={[["21"], ["22"], ["23"], ["24"], ["25"], ["26"]]}
import { defineRouteConfig } from "@medusajs/admin-shared"
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 items 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, its highly recommended that you use the [Medusa UI package](https://docs.medusajs.com/ui) to match your pages 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"
const CustomPage = () => {
@@ -101,6 +74,13 @@ export const config = defineRouteConfig({
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 items 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
@@ -134,13 +114,7 @@ This adds a page under the path `/app/settings/custom`. An item is also added to
## Path Parameters
A UI route can accept path parameters if the name of any of the directories in its path is of the format `[param]`. For example, `src/admin/routes/custom/[id]/page.tsx`.
To retrieve the path parameters, install the `react-router-dom` to use its `useParams` hook:
```bash npm2yarn
npm install react-router-dom
```
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, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content:
@@ -157,4 +131,6 @@ const 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.
@@ -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:
export const widgetHighlights = [
["4", "ProductWidget", "The React component of the product widget."],
["14", "zone", "The zone to inject the widget to."]
["5", "ProductWidget", "The React component of the product widget."],
["15", "zone", "The zone to inject the widget to."]
]
```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights}
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 products details.
### Test the Widget
To test out the widget, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, open a products details page. Youll 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
Its highly recommended that you use the [Medusa UI package](https://docs.medusajs.com/ui) to match your widgets 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"
// The widget
const ProductWidget = () => {
return (
<Container>
@@ -121,18 +40,78 @@ const ProductWidget = () => {
)
}
export const config: WidgetConfig = defineWidgetConfig({
zone: "product.details.after",
// The widget's configurations
export const config = defineWidgetConfig({
zone: "product.details.before",
})
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 products details.
---
## Test the Widget
To test out the widget, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, open a products details page. Youll 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, youll learn about the CORS middleware and how to configure i
## 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
@@ -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.
<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
@@ -70,13 +78,16 @@ You can do that in the exported middlewares configurations in `src/api/middlewar
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 {
ConfigModule,
MiddlewaresConfig,
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import { ConfigModule } from "@medusajs/types"
import { parseCorsOrigins } from "@medusajs/utils"
import cors from "cors"
@@ -85,7 +96,11 @@ export const config: MiddlewaresConfig = {
{
matcher: "/custom*",
middlewares: [
(req, res, next) => {
(
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => {
const configModule: 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.
## 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`.
@@ -41,5 +41,5 @@ export const POST = (
This adds two API Routes:
- A `GET` route at `localhost:9000/store/hello-world`.
- A `POST` route at `localhost:9000/store/hello-world`.
- A `GET` route at `http://localhost:9000/store/hello-world`.
- A `POST` route at `http://localhost:9000/store/hello-world`.
@@ -8,7 +8,7 @@ In this chapter, youll learn about middlewares and how to create them.
## What is a Middleware?
A middleware is a function executed when a request is sent to an API Route.
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:
```ts title="src/api/middlewares.ts"
import { MiddlewaresConfig } from "@medusajs/medusa"
import type {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
MiddlewaresConfig,
} from "@medusajs/medusa"
export const config: MiddlewaresConfig = {
routes: [
{
matcher: "/store*",
middlewares: [
(req, res, next) => {
(
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => {
console.log("Received a request!")
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`.
<Note>
---
The `matcher` property can be a string or a regular expression.
</Note>
### Test Middleware
## 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:
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}
import { MiddlewaresConfig } from "@medusajs/medusa"
```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import type {
MedusaNextFunction,
MedusaRequest,
MedusaResponse,
MiddlewaresConfig,
} from "@medusajs/medusa"
export const config: MiddlewaresConfig = {
routes: [
@@ -110,7 +123,11 @@ export const config: MiddlewaresConfig = {
matcher: "/store*",
method: ["POST", "PUT"],
middlewares: [
(req, res, next) => {
(
req: MedusaRequest,
res: MedusaResponse,
next: MedusaNextFunction
) => {
console.log("Received a request!")
next()
@@ -8,7 +8,7 @@ In this chapter, youll learn about path, query, and request body parameters.
## Path Parameters
To create an API route that accepts a path parameter, create a directory within the route'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:
@@ -16,7 +16,7 @@ export const singlePathHighlights = [
["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 {
MedusaRequest,
MedusaResponse,
@@ -45,7 +45,7 @@ export const multiplePathHighlights = [
["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 {
MedusaRequest,
MedusaResponse,
@@ -77,7 +77,7 @@ export const queryHighlights = [
["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 {
MedusaRequest,
MedusaResponse,
@@ -6,10 +6,14 @@ export const metadata = {
In this chapter, youll 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.
---
## Default Protected Routes
Medusa applies an authentication guard on the following routes:
- 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
```
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
You can access the logged-in customers ID in all API routes starting with `/store` using the `user.customer_id` property of the `MedusaRequest` object.
You can access the logged-in customers ID in all API routes starting with `/store` using the `auth_context.actor_id` property of the `MedusaRequest` object.
For example:
@@ -67,7 +71,7 @@ export const GET = async (
ModuleRegistrationName.CUSTOMER
)
const customer = await customerModuleService.retrieve(
const customer = await customerModuleService.retrieveCustomer(
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
You can access the logged-in admin users ID in all API Routes starting with `/admin` using the `user.userId` property of the `MedusaRequest` object.
You can access the logged-in admin users ID in all API Routes starting with `/admin` using the `auth_context.actor_id` property of the `MedusaRequest` object.
For example:
@@ -97,17 +101,19 @@ export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const userService: IUserModuleService = req.scope.resolve(
const userModuleService: IUserModuleService = req.scope.resolve(
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.
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:
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.
2. `allowUnregistered`: (default: `false`) A boolean indicating whether new users can be authenticated.
3. An optional object of configurations accepting the following property:
- `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.
@@ -8,7 +8,7 @@ In this chapter, you'll learn how create and execute custom scripts from Medusa'
## 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:
```ts title="src/scripts/my-script.ts"
import { ExecArgs, IProductModuleService } from "@medusajs/types"
import {
ExecArgs,
IProductModuleService,
} from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/utils"
export default async function myScript({ container }: ExecArgs) {
@@ -27,7 +30,8 @@ export default async function myScript({ container }: ExecArgs) {
ModuleRegistrationName.PRODUCT
)
const [, count] = await productModuleService.listAndCount()
const [, count] = await productModuleService
.listAndCountProducts()
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:
```bash npm2yarn
```bash
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:
```bash npm2yarn
```bash
npx medusa exec ./src/scripts/my-script.ts arg1 arg2
```
@@ -4,11 +4,11 @@ export const metadata = {
# {metadata.title}
In this chapter, youll learn how to configure data model properties, such as setting their default value.
In this chapter, youll learn how to configure data model properties.
## Propertys 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:
@@ -62,7 +62,7 @@ export default MyCustom
## Unique Property
The `unique` method indicates that a propertys value must be unique in the database.
The `unique` method indicates that a propertys value must be unique in the database through a unique index.
For example:
@@ -8,7 +8,7 @@ In this chapter, youll learn how to define indices on a data model.
## 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:
@@ -38,12 +38,12 @@ In this example, you define an index on the `name` property.
## 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:
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."]
]
@@ -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
```
By default, this property is considered to be the data models primary key.
---
## text
@@ -180,7 +178,7 @@ export default MyCustom
## array
The `array` method defines an array or strings property.
The `array` method defines an array of strings property.
For example:
@@ -6,6 +6,12 @@ export const metadata = {
In this chapter, youll 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?
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
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:
@@ -6,7 +6,7 @@ export const metadata = {
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, theyre emitted with a data payload.
@@ -40,6 +40,8 @@ export const config: SubscriberConfig = {
This logs the product ID received in the `product.created` events data payload to the console.
---
## List of Events with Data Payload
Refer to [this reference](!resources!/events-reference) for a full list of events emitted by Medusa and their data payloads.
@@ -1,87 +0,0 @@
export const metadata = {
title: `${pageNumber} Loaders Outside Modules`,
}
# {metadata.title}
In this chapter, youll 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 youre performing a task on application start-up, but dont 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
```
Youll 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 modules 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
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:
@@ -64,5 +63,4 @@ export default function helloWorldLoader({
logger.info("[helloWorldLoader]: Hello, World!")
}
```
@@ -4,12 +4,12 @@ export const metadata = {
# {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">
- 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>
@@ -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.
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.
@@ -1,23 +0,0 @@
export const metadata = {
title: `${pageNumber} Link Modules`,
}
# {metadata.title}
In this chapter, youll 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 Medusas commerce modules.
</Note>
@@ -0,0 +1,128 @@
export const metadata = {
title: `${pageNumber} Module Link`,
}
# {metadata.title}
In this chapter, youll 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 modules loaders receive as a parameter has an `options` pro
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 {
LoaderOptions,
} from "@medusajs/modules-sdk"
@@ -6,9 +6,15 @@ export const metadata = {
In this chapter, youll 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?
The remote link is a class with utility methods to manage links defined by the link module. Its 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. Its registered in the Medusa container under the `remoteLink` registration name.
For example:
@@ -38,7 +44,9 @@ export async function POST(
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.
@@ -51,23 +59,25 @@ import { Modules } from "@medusajs/utils"
await remoteLink.create({
[Modules.PRODUCT]: {
variant_id: product.variants[0].id,
product_id: "prod_123",
},
[Modules.PRICING]: {
price_set_id: price.id,
"hello": {
my_custom_id: "mc_123",
},
})
```
The `create` method accepts as a parameter an object. The objects keys are the names of the linked modules.
The value of each modules property is an object. It defines the values of the linked fields.
The value of each modules 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 doesnt 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:
@@ -78,19 +88,21 @@ import { Modules } from "@medusajs/utils"
await remoteLink.dismiss({
[Modules.PRODUCT]: {
variant_id: product.variants[0].id,
product_id: "prod_123",
},
[Modules.PRICING]: {
price_set_id: price.id,
"hello": {
my_custom_id: "mc_123",
},
})
```
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:
@@ -103,16 +115,18 @@ await productModuleService.deleteVariants([variant.id])
await remoteLink.delete({
[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:
@@ -121,77 +135,11 @@ import { Modules } from "@medusajs/utils"
// ...
await productModuleService.restoreVariants([variant.id])
await productModuleService.restoreProducts(["prod_123"])
await remoteLink.restore({
[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. Its 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 Medusas 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.
For example:
```js
module.exports = defineConfig({
// ...
modules: {
helloModuleService: {
resolve: "./modules/hello",
definition: {
isQueryable: true,
},
},
},
})
```
</Note>
---
@@ -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
```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({
my_customs: result,
})
@@ -12,9 +12,9 @@ In this chapter, youll learn about what the service factory is and how to use
Medusa provides a service factory that your modules 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.
@@ -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.
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
@@ -4,15 +4,12 @@ export const metadata = {
# {metadata.title}
In the previous chapters, you got a brief introduction to Medusas 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 Medusas 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, youll be able to:
- Expose API routes with control over authentication, parsing request bodies, and more.
- Manage data models in services.
- Create relationships between modules.
- Create data models with complex fields and relations.
- Create loaders outside of modules.
- Access events payloads.
- Expose API routes with control over authentication.
- Build sophisticated business logic in modules and manage links between them.
- Create advanced workflows and configure retries and timeout.
- 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}
In this document, youll learn how to access errors that occur during a workflows execution.
In this chapter, youll learn how to access errors that occur during a workflows 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.
@@ -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:
```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 products ID.
@@ -40,17 +41,9 @@ Create the file `src/workflows/update-product-erp/steps/update-product.ts` with
export const updateProductHighlights = [
["13", "resolve", "Resolve the `ProductService` from the Medusa container."],
[
"16",
"previousProductData",
"Retrieve the `previousProductData` to pass it to the compensation function.",
],
["19", "", "Update the product."],
[
"39",
"",
"Revert the products data using the `previousProductData` passed from the step to the compensation function.",
],
["16", "previousProductData", "Retrieve the `previousProductData` to pass it to the compensation function."],
["19", "updateProducts", "Update the product."],
["39", "updateProducts", "Revert the products 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"
@@ -66,9 +59,10 @@ const updateProduct = createStep(
context.container.resolve(ModuleRegistrationName.PRODUCT)
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, {
// pass to compensation function
@@ -82,10 +76,12 @@ const updateProduct = createStep(
const { id, type, options, variants, ...previousData } = previousProductData
await productModuleService.update(id, {
...previousData,
variants: variants.map((variant) => {
const variantOptions = {}
await productModuleService.updateProducts(
id,
{
...previousData,
variants: variants.map((variant) => {
const variantOptions = {}
variant.options.forEach((option) => {
variantOptions[option.option.title] = option.value
@@ -110,7 +106,7 @@ export default updateProduct
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 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"
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 updateErp from "./steps/update-erp"
export type UpdateProductAndErpWorkflowInput = UpdateProductDTO
export type UpdateProductAndErpWorkflowInput = UpsertProductDTO
type WorkflowOutput = {
product: ProductDTO
@@ -8,7 +8,7 @@ In this chapter, you'll learn how to add a compensation function to a step.
## Compensation Function
Errors can occur in a workflow. To avoid data inconsistency, 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:
@@ -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. Its useful to undo or roll back actions youve performed in a step.
Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow. Its useful to undo or roll back actions youve performed in a step.
### Test Compensation Function
---
## Test the Compensation Function
1. Add another step that throws an error:
@@ -107,7 +109,7 @@ npm run dev
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
```
@@ -4,7 +4,7 @@ export const metadata = {
# {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
@@ -28,6 +28,8 @@ const myWorkflow = createWorkflow<
})
```
---
## No Async Functions
The function passed to the `createWorkflow` cant be an async function:
@@ -50,6 +52,8 @@ const myWorkflow = createWorkflow<
})
```
---
## No Direct Data Manipulation
Since the constructor function only defines how the workflow works, you cant 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:
```ts title="src/workflows/hello-world.ts" highlights={[["13"]]} collapsibleLines="1-10" expandButtonLabel="Show More"
import { createStep, createWorkflow } from "@medusajs/workflows-sdk"
```ts title="src/workflows/hello-world.ts" highlights={[["14"]]} collapsibleLines="1-10" expandButtonLabel="Show More"
import {
createStep,
createWorkflow,
StepResponse,
} from "@medusajs/workflows-sdk"
const step1 = createStep("step-1", async () => {
// ...
return new StepResponse({})
})
const step2 = createStep(
@@ -35,74 +39,90 @@ const step2 = createStep(
async: true,
},
async () => {
// ...
return new StepResponse({})
}
)
const step3 = createStep("step-3", async () => {
// ...
return new StepResponse("Finished three steps")
})
type WorkflowOutput = {
message: string
}
const myWorkflow = createWorkflow<{}, WorkflowOutput>(
{
name: "hello-world",
},
function () {
step1()
step2()
step3()
const myWorkflow = createWorkflow<
{},
WorkflowOutput
>("hello-world", function () {
step1()
step2()
const message = step3()
return {
message,
}
)
})
export default myWorkflow
```
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.
---
## 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.
For example:
export const highlights = [
["18", "", "Resolve the workflow engine from the Medusa container."],
["24", "subscribe", "Subscribe to status changes of the workflow execution."],
["18", "resolve", "Resolve the workflow engine from the Medusa container."],
["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"
import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
import myWorkflow from "../../../workflows/hello-world"
import { IWorkflowEngineService } from "@medusajs/workflows-sdk"
import {
IWorkflowEngineService,
} from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/utils"
export async function GET(req: MedusaRequest, res: MedusaResponse) {
const { transaction, result } = await myWorkflow(req.scope).run()
const workflowEngine = req.scope.resolve<IWorkflowEngineService>(
const workflowEngineModuleService = req.scope.resolve<
IWorkflowEngineService
>(
ModuleRegistrationName.WORKFLOW_ENGINE
)
await workflowEngine.subscribe({
const subscriptionOptions = {
workflowId: "hello-world",
transactionId: transaction.transactionId,
subscriber: (data) => {
subscriberId: "hello-world-subscriber",
}
await workflowEngineModuleService.subscribe({
...subscriptionOptions,
subscriber: async (data) => {
if (data.eventType === "onFinish") {
console.log("Finished execution", data.result)
// unsubscribe
await workflowEngineModuleService.unsubscribe({
...subscriptionOptions,
subscriberOrId: subscriptionOptions.subscriberId,
})
} else if (data.eventType === "onStepFailure") {
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 workflows 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, youll learn how to run workflow steps in parallel.
## parallelize Utility Function
If your workflow has steps that dont rely on one anothers 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 dont rely on one anothers 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:
@@ -55,4 +55,4 @@ const myWorkflow = createWorkflow<
The `parallelize` function accepts the steps to run in parallel as a parameter.
It returns an array of each steps 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, youll 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.
You can configure the step to retry on failure. The `createStep` function can accept a configuration object instead of the steps 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 steps 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 {
createStep,
StepResponse,
createWorkflow,
} from "@medusajs/workflows-sdk"