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:
@@ -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.
|
||||
|
||||

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

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

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

|
||||
|
||||
---
|
||||
|
||||
## Other Lifecycle Stages
|
||||
|
||||
The following lifecycle stages occur only in specific cases:
|
||||
|
||||
- `canceled`: If you cancel a batch job using the [API route](https://docs.medusajs.com/api/admin#batch-jobs_postbatchjobsbatchjobcancel) or `BatchJobService`'s `cancel` method, the batch job’s status is changed to `canceled` and the `batch.canceled` event is emitted.
|
||||
- `failed`: If an error occurs during any stage, the batch job’s status is changed to `failed` and the `batch.failed` event is emitted.
|
||||
@@ -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
|
||||
|
||||
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 file’s default export must be the UI route’s React component.
|
||||
|
||||
For example, create the file `src/admin/routes/custom/page.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/routes/custom/page.tsx"
|
||||
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 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:
|
||||
|
||||
@@ -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 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"
|
||||
|
||||
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 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
|
||||
@@ -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 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"
|
||||
|
||||
// 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 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
|
||||
|
||||
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, you’ll learn about middlewares and how to create them.
|
||||
|
||||
## What is a Middleware?
|
||||
|
||||
A middleware is a function executed when a request is sent to an API Route.
|
||||
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, you’ll 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, 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.
|
||||
|
||||
---
|
||||
|
||||
## 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 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:
|
||||
|
||||
@@ -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 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:
|
||||
|
||||
@@ -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, 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
|
||||
|
||||
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 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:
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ In this chapter, you’ll 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 model’s 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, 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?
|
||||
|
||||
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, 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.
|
||||
|
||||
---
|
||||
|
||||
## 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, 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
|
||||
|
||||
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, 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.
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||
```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, 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?
|
||||
|
||||
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:
|
||||
|
||||
@@ -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 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:
|
||||
|
||||
@@ -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. 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.
|
||||
|
||||
---
|
||||
<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, 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.
|
||||
|
||||
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 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:
|
||||
|
||||
- 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, 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.
|
||||
|
||||
|
||||
@@ -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 product’s 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 product’s 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 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"
|
||||
@@ -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. 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:
|
||||
|
||||
@@ -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` can’t 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 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:
|
||||
|
||||
```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 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
|
||||
|
||||
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:
|
||||
|
||||
@@ -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 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.
|
||||
|
||||
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 {
|
||||
createStep,
|
||||
StepResponse,
|
||||
createWorkflow,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user