docs: add routing page (#9550)
- Add a new homepage to `book` project for the routing page - Move all main doc pages to be under `/v2/learn` (and added redirects + fixed links across docs) - Other: add admin components to resources dropdown + fixes to search on mobile. Closes DX-955 Preview: https://docs-v2-git-docs-router-page-medusajs.vercel.app/v2
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Admin Customizations`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to customize the Medusa Admin dashboard.
|
||||
|
||||
## What is the Medusa Admin?
|
||||
|
||||
The Medusa Admin is an admin dashboard that merchants use to manage their store's data.
|
||||
|
||||
You can extend the Medusa Admin to add widgets and new pages. In your customizations, you interact with API routes to provide merchants with custom functionalities.
|
||||
|
||||
The Medusa Admin is installed in your Medusa application and runs at `http://localhost:9000/app` when you start the application.
|
||||
|
||||
---
|
||||
|
||||
## Example: Create a Widget
|
||||
|
||||
A widget is a React component that can be injected into an existing page in the admin dashboard.
|
||||
|
||||
For example, create the file `src/admin/widgets/product-widget.tsx` with the following content:
|
||||
|
||||
```tsx title="src/admin/widgets/product-widget.tsx"
|
||||
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
|
||||
const ProductWidget = () => {
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">Product Widget</Heading>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const config = defineWidgetConfig({
|
||||
zone: "product.details.before",
|
||||
})
|
||||
|
||||
export default ProductWidget
|
||||
```
|
||||
|
||||
This inserts a widget with the text “Product Widget” at the beginning of a product’s details page.
|
||||
|
||||
In your widget, use custom components from the [Medusa UI package](https://docs.medusajs.com/ui).
|
||||
|
||||
### Test the Widget
|
||||
|
||||
To test out the widget, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, open a product’s details page in the Medusa Admin. You’ll find your custom widget at the top of the page.
|
||||
|
||||
---
|
||||
|
||||
## Admin Components List
|
||||
|
||||
To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](!resources!/admin-component) to find common components.
|
||||
@@ -0,0 +1,63 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} API Routes`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn what API Routes are and how to create them.
|
||||
|
||||
## What is an API Route?
|
||||
|
||||
An API Route is a REST API endpoint. It exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems.
|
||||
|
||||
The Medusa core application provides a set of admin and store API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities.
|
||||
|
||||
---
|
||||
|
||||
## How to Create an API Route?
|
||||
|
||||
An API Route is created in a TypeScript or JavaScript file under the `src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`.
|
||||
|
||||
Each file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…).
|
||||
|
||||
For example, to create a `GET` API Route at `/hello-world`, create the file `src/api/hello-world/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/hello-world/route.ts"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
|
||||
export const GET = (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
res.json({
|
||||
message: "[GET] Hello world!",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Test API Route
|
||||
|
||||
To test the API route above, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, send a `GET` request to the `/hello-world` API Route:
|
||||
|
||||
```bash
|
||||
curl http://localhost:9000/hello-world
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use API Routes
|
||||
|
||||
<Note title="Use API routes when" type="success">
|
||||
|
||||
You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,55 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Commerce Modules`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about Medusa's commerce modules.
|
||||
|
||||
## What is a Commerce Module?
|
||||
|
||||
Medusa provides all its commerce features as separate commerce modules, such as the Product or Order modules. Medusa uses these modules in its API routes to expose their commerce features.
|
||||
|
||||
Medusa's commerce modules and your custom modules are interchangeable in the Medusa application, making Medusa’s architecture more flexible.
|
||||
|
||||
### List of Medusa's Commerce Modules
|
||||
|
||||
Refer to [this reference](!resources!/commerce-modules) for a full list of commerce modules in Medusa.
|
||||
|
||||
---
|
||||
|
||||
## Resolve Commerce Module Services
|
||||
|
||||
Similarly to your custom module, a commerce module's main service is registered in the Medusa container. So, you can resolve it in your resources, such as API routes, to use its functionality.
|
||||
|
||||
For example, you saw this code snippet in the [Medusa container chapter](../medusa-container/page.mdx):
|
||||
|
||||
```ts highlights={[["10"]]}
|
||||
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import { IProductModuleService } from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const productModuleService: IProductModuleService = req.scope.resolve(
|
||||
Modules.PRODUCT
|
||||
)
|
||||
|
||||
const [, count] = await productModuleService
|
||||
.listAndCountProducts()
|
||||
|
||||
res.json({
|
||||
count,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
When you resolve the `Modules.PRODUCT` (or `productModuleService`) registration name, you're actually resolving the main service of the Product Module.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
To resolve the main service of any commerce module, use the registration name defined in the `Modules` enum imported from `@medusajs/framework/utils`.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,123 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Events and Subscribers`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn how to handle events with subscribers.
|
||||
|
||||
## What is an Event?
|
||||
|
||||
When an action is performed in Medusa, such as creating a product, the Medusa application emits an event.
|
||||
|
||||
You can listen to those events and perform an asynchronous action using a subscriber.
|
||||
|
||||
---
|
||||
|
||||
## What is a Subscriber?
|
||||
|
||||
A subscriber is a function executed whenever the event it listens to is emitted.
|
||||
|
||||
### How to Create a Subscriber?
|
||||
|
||||
A subscriber is created in a TypeScript or JavaScript file under the `src/subscribers` directory.
|
||||
|
||||
For example, create the file `src/subscribers/product-created.ts` with the following content:
|
||||
|
||||
```ts title="src/subscribers/product-created.ts"
|
||||
import { type SubscriberConfig } from "@medusajs/framework"
|
||||
|
||||
// subscriber function
|
||||
export default async function productCreateHandler() {
|
||||
console.log("A product was created")
|
||||
}
|
||||
|
||||
// subscriber config
|
||||
export const config: SubscriberConfig = {
|
||||
event: "product.created",
|
||||
}
|
||||
```
|
||||
|
||||
A subscriber file must export:
|
||||
|
||||
- A subscriber function that is an asynchronous function executed whenever the associated event is triggered.
|
||||
- A configuration object defining the event this subscriber is listening to.
|
||||
|
||||
The above subscriber listens to the `product.created` event. Whenever the event is emitted, it logs in the terminal `A product is created`.
|
||||
|
||||
---
|
||||
|
||||
## Test the Subscriber
|
||||
|
||||
To test the subscriber, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, go to the Medusa Admin at `localhost:9000/app` and create a product. You’ll see the following messages logged in the Medusa application's terminal:
|
||||
|
||||
```bash
|
||||
info: Processing product.created which has 1 subscribers
|
||||
A product was created
|
||||
```
|
||||
|
||||
The first message indicates that the `product.created` event was emitted, and the second one is the message logged from the subscriber.
|
||||
|
||||
---
|
||||
|
||||
## When to Use Subscribers
|
||||
|
||||
<Note title="Use subscribers when" type="success">
|
||||
|
||||
You want to perform an action everytime a specific event is emitted in the Medusa application.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Resolve Resources
|
||||
|
||||
The subscriber function accepts an object parameter with the property `container`. Its value is the Medusa container, and you can use it to resolve other resources, such as services.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["7", "container", "Recieve the Medusa Container in the object parameter."],
|
||||
["10", "resolve", "Resolve the Product Module's main service."],
|
||||
["10", "Modules.PRODUCT", "The module's registration name imported from `@medusajs/framework/utils`."]
|
||||
]
|
||||
|
||||
```ts title="src/subscribers/product-created.ts" highlights={highlights}
|
||||
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
|
||||
import { IProductModuleService } from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
export default async function productCreateHandler({
|
||||
event: { data },
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const productModuleService: IProductModuleService =
|
||||
container.resolve(Modules.PRODUCT)
|
||||
|
||||
const productId = data.id
|
||||
|
||||
const product = await productModuleService.retrieveProduct(
|
||||
productId
|
||||
)
|
||||
|
||||
console.log(`The product ${product.title} was created`)
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: `product.created`,
|
||||
}
|
||||
```
|
||||
|
||||
You use the container to resolve the Product Module's main service, then log the title of the created product.
|
||||
|
||||
---
|
||||
|
||||
## Events List
|
||||
|
||||
Find a list of all emitted events in [this reference](!resources!/events-reference).
|
||||
@@ -0,0 +1,78 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Loaders`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about loaders and how to use them.
|
||||
|
||||
## What is a Loader?
|
||||
|
||||
A loader is a function executed when the Medusa application starts. You define and export it in a module.
|
||||
|
||||
Loaders are useful to perform a task at the application start-up, such as to sync data between Medusa and a third-pary service.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Loader?
|
||||
|
||||
A loader is created in a TypeScript or JavaScript file under a module's `loaders` directory.
|
||||
|
||||
For example, create the file `src/modules/hello/loaders/hello-world.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/loaders/hello-world.ts"
|
||||
export default async function helloWorldLoader() {
|
||||
console.log(
|
||||
"[HELLO MODULE] Just started the Medusa application!"
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Export Loader in Module Definition
|
||||
|
||||
Import the loader in `src/modules/hello/index.ts` and export it in the module's definition:
|
||||
|
||||
```ts title="src/modules/hello/index.ts"
|
||||
// other imports...
|
||||
import helloWorldLoader from "./loaders/hello-world"
|
||||
|
||||
export default Module("hello", {
|
||||
// ...
|
||||
loaders: [helloWorldLoader],
|
||||
})
|
||||
```
|
||||
|
||||
The value of the `loaders` property is an array of loader functions.
|
||||
|
||||
---
|
||||
|
||||
## Test the Loader
|
||||
|
||||
Start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Among the messages logged in the terminal, you’ll see the following message:
|
||||
|
||||
```bash
|
||||
[HELLO MODULE] Just started the Medusa application!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use Loaders
|
||||
|
||||
<Note title="Use loaders when" type="success">
|
||||
|
||||
- You're performing an action at application start-up.
|
||||
- You're establishing a one-time connection with an external system.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Don't use loaders if" type="error">
|
||||
|
||||
You want to perform an action continuously or at a set time pattern in the application. Use scheduled jobs instead, which is explained in an upcoming chapter.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,46 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Medusa container`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about the Medusa container and how to use it.
|
||||
|
||||
## What is the Medusa container?
|
||||
|
||||
The Medusa container holds all resources registered in the Medusa application, such as services.
|
||||
|
||||
In your customizations, you use the Medusa container to resolve these resources and use their functionalities.
|
||||
|
||||
For example, in a custom API route you can resolve any service registered in the Medusa application using the `scope.resolve` method of the `MedusaRequest` parameter:
|
||||
|
||||
export const highlights = [
|
||||
["9", "resolve", "Resolve the Product Module's main service."],
|
||||
[
|
||||
"10",
|
||||
"Modules.PRODUCT",
|
||||
"The resource registration name imported from `@medusajs/framework/utils`.",
|
||||
],
|
||||
]
|
||||
|
||||
```ts highlights={highlights}
|
||||
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import { IProductModuleService } from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
export const GET = async (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const productModuleService: IProductModuleService = req.scope.resolve(
|
||||
Modules.PRODUCT
|
||||
)
|
||||
|
||||
const [, count] = await productModuleService
|
||||
.listAndCountProducts()
|
||||
|
||||
res.json({
|
||||
count,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Modules Directory Structure`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this document, you'll learn about the expected files and directories in your module.
|
||||
|
||||

|
||||
|
||||
## index.ts
|
||||
|
||||
The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](../modules/page.mdx).
|
||||
|
||||
---
|
||||
|
||||
## service.ts
|
||||
|
||||
A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](../modules/page.mdx).
|
||||
|
||||
---
|
||||
|
||||
## Other Directories
|
||||
|
||||
The following directories are optional and their content are explained more in the following chapters:
|
||||
|
||||
- `models`: Holds the data models representing tables in the database.
|
||||
- `migrations`: Holds the migration files used to reflect changes on the database.
|
||||
- `loaders`: Holds the scripts to run on the Medusa application's start-up.
|
||||
@@ -0,0 +1,266 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Modules`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about modules and how to create them.
|
||||
|
||||
## What is a Module?
|
||||
|
||||
A module is a package of reusable commerce or architectural functionalities.
|
||||
|
||||
In Medusa, modules handle business logic in a class called a service, and define and manage data models that represent tables in the database.
|
||||
|
||||
Out of the box, Medusa comes with multiple pre-built modules for core commerce needs. For example, the Cart Module holds the data models and business logic for cart operations.
|
||||
|
||||
As you learn more about Medusa, you will see that Modules are central to customizations and integrations.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Module?
|
||||
|
||||
In this section, you'll build a module that has a `MyCustom` data model and a service to manage that data model. You'll then use the module's service in an API route to create a record of `MyCustom`.
|
||||
|
||||
Modules are created in a sub-directory of `src/modules`.
|
||||
|
||||
For example, create the directory `src/modules/hello`.
|
||||
|
||||
### 1. Create Data Model
|
||||
|
||||
A data model represents a table in the database. It's created in a TypeScript or JavaScript file under the module's `models` directory.
|
||||
|
||||
For example, create the file `src/modules/hello/models/my-custom.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/models/my-custom.ts"
|
||||
import { model } from "@medusajs/framework/utils"
|
||||
|
||||
const MyCustom = model.define("my_custom", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text(),
|
||||
})
|
||||
|
||||
export default MyCustom
|
||||
```
|
||||
|
||||
You define the data model using the `define` method of the `model` utility imported from `@medusajs/framework/utils`. It accepts two parameters:
|
||||
|
||||
1. The first one is the name of the data model's table in the database. It should be snake-case.
|
||||
2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods.
|
||||
|
||||
The example above defines the data model `MyCustom` with the properties `id` and `name`.
|
||||
|
||||
<Note>
|
||||
|
||||
Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`.
|
||||
|
||||
</Note>
|
||||
|
||||
### 2. Create Service
|
||||
|
||||
A module must define a service that implements its functionalities, such as manage the records of your custom data models in the database.
|
||||
|
||||
A service is a TypeScript or JavaScript class defined in the `service.ts` file at the root of your module's directory.
|
||||
|
||||
For example, create the file `src/modules/hello/service.ts` with the following content:
|
||||
|
||||
export const highlights = [
|
||||
["4", "MedusaService", "The service factory function."],
|
||||
["5", "MyCustom", "The data models to generate data-management methods for."]
|
||||
]
|
||||
|
||||
```ts title="src/modules/hello/service.ts" highlights={highlights}
|
||||
import { MedusaService } from "@medusajs/framework/utils"
|
||||
import MyCustom from "./models/my-custom"
|
||||
|
||||
class HelloModuleService extends MedusaService({
|
||||
MyCustom,
|
||||
}){
|
||||
}
|
||||
|
||||
export default HelloModuleService
|
||||
```
|
||||
|
||||
In the snippet above, your module's service extends a class generated by the `MedusaService` utility function, which is the service factory.
|
||||
|
||||
The `MedusaService` function accepts as a parameter an object of data models, and returns a class with generated methods for data-management Create, Read, Update, and Delete (CRUD) operations on those data models.
|
||||
|
||||
For example, `HelloModuleService` now has a `createMyCustoms` method to create `MyCustom` records, and `retrieveMyCustom` to retrive a `MyCustom` record.
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
If a module doesn't have data models, it doesn't need to extend `MedusaService`.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
|
||||
You'll learn more about the methods generated by the service factory in later chapters.
|
||||
|
||||
</Note>
|
||||
|
||||
### 3. Export Module Definition
|
||||
|
||||
A module must have an `index.ts` file in its root directory. The file exports the module's definition.
|
||||
|
||||
For example, create the file `src/modules/hello/index.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/index.ts" highlights={[["7", "", "The main service of the module."]]}
|
||||
import HelloModuleService from "./service"
|
||||
import { Module } from "@medusajs/framework/utils"
|
||||
|
||||
export const HELLO_MODULE = "helloModuleService"
|
||||
|
||||
export default Module(HELLO_MODULE, {
|
||||
service: HelloModuleService,
|
||||
})
|
||||
```
|
||||
|
||||
You use the `Module` function imported from `@medusajs/framework/utils` to create the module's definition. It accepts two parameters:
|
||||
|
||||
1. The name that the module's main service is registered under (`helloModuleService`).
|
||||
2. An object with a required property `service` indicating the module's main service.
|
||||
|
||||
### 4. Add Module to Configurations
|
||||
|
||||
The last step is to add the module in Medusa’s configurations.
|
||||
|
||||
In `medusa-config.ts`, add a `modules` property and pass in it your custom module:
|
||||
|
||||
```ts title="medusa-config.ts" highlights={[["7"]]}
|
||||
module.exports = defineConfig({
|
||||
projectConfig: {
|
||||
// ...
|
||||
},
|
||||
modules: [
|
||||
{
|
||||
resolve: "./src/modules/hello",
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
Its value is an array of objects, each having a `resolve` property, whose value is either a path to module's directory, or an `npm` package’s name.
|
||||
|
||||
### 5. Generate Migrations
|
||||
|
||||
A migration is a TypeScript or JavaScript file that defines database changes made by your module, such as create the `my_custom` table for the `MyCustom` data model.
|
||||
|
||||
To generate a migration for the data models in your module, run the following command:
|
||||
|
||||
```bash
|
||||
npx medusa db:generate helloModuleService
|
||||
```
|
||||
|
||||
The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for.
|
||||
|
||||
<Note>
|
||||
|
||||
The module name `helloModuleService` is the key used when registering the module in Medusa's `modules` configuration.
|
||||
|
||||
</Note>
|
||||
|
||||
The above command creates a migration file at the directory `src/modules/hello/migrations` similar to the following:
|
||||
|
||||
```ts
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20240702105919 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql("create table if not exists \"my_custom\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"my_custom_pkey\" primary key (\"id\"));")
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql("drop table if exists \"my_custom\" cascade;")
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
In the migration class, the `up` method creates the table `my_custom` and defines its columns using PostgreSQL syntax. The `down` method drops the table.
|
||||
|
||||
### 6. Run Migrations
|
||||
|
||||
To reflect the changes in the generated migration file, run the `db:migrate` command:
|
||||
|
||||
```bash
|
||||
npx medusa db:migrate
|
||||
```
|
||||
|
||||
This creates the `my_custom` table in the database.
|
||||
|
||||
---
|
||||
|
||||
## Test the Module
|
||||
|
||||
Since the module's main service is registered in the Medusa container, you can resolve it in other resources to use its methods.
|
||||
|
||||
For example, create the API route `src/api/custom/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/custom/route.ts"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||
import HelloModuleService from "../../modules/hello/service"
|
||||
import { HELLO_MODULE } from "../../modules/hello"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService = req.scope.resolve(
|
||||
HELLO_MODULE
|
||||
)
|
||||
|
||||
const my_custom = await helloModuleService.createMyCustoms({
|
||||
name: "test"
|
||||
})
|
||||
|
||||
res.json({
|
||||
my_custom
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
You resolve the Hello Module's main service and use its generated method `createMyCustoms` to create a new record in the database, then return that record.
|
||||
|
||||
Then, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Finally, send a `GET` request to `/custom`:
|
||||
|
||||
```bash
|
||||
curl http://localhost:9000/custom
|
||||
```
|
||||
|
||||
You’ll receive the following response:
|
||||
|
||||
```json
|
||||
{
|
||||
"my_custom": {
|
||||
"id": "123...",
|
||||
"name": "test",
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why Use Modules
|
||||
|
||||
In digital commerce, you often need to introduce custom behavior specific to your products, industry, tech stack, or your general ways of working. In other commerce platforms, introducing custom business logic and data models requires setting up separate applications to manage these customizations.
|
||||
|
||||
Medusa removes this overhead by allowing you to easily write custom Modules that integrate into the Medusa application without implications on the existing setup.
|
||||
|
||||
<Note title="Use modules when" type="success">
|
||||
|
||||
- You're adding a new table to the database.
|
||||
- You're extending an existing table in the database to add custom fields, which is explained in later chapters.
|
||||
- You're integrating a third-party system for commerce or architectural features, as explained in later chapters.
|
||||
- You want to re-use your custom commerce functionalities across Medusa applications or use them in other environments, such as Edge functions and Next.js apps.
|
||||
|
||||
</Note>
|
||||
@@ -0,0 +1,18 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} The Basics`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In the next chapters, you’ll learn about the basic concepts of Medusa that are central in your development.
|
||||
|
||||
By the end of these chapter, you’ll be able to:
|
||||
|
||||
- Expose your custom functionalities through endpoints.
|
||||
- Create custom modules that define custom business logic.
|
||||
- Create custom tables in the database through data models.
|
||||
- Execute scripts when the Medusa application starts.
|
||||
- Perform asynchronus actions when an event occurs.
|
||||
- Run tasks at a specified time or pattern during the Medusa application's runtime.
|
||||
- Create custom flows as a series of steps involving multiple services.
|
||||
- Customize the admin dashboard to inject components on existing pages or add new pages.
|
||||
@@ -0,0 +1,23 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Project File Conventions`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about important directories and files in your Medusa application's project.
|
||||
|
||||
## src
|
||||
|
||||
This directory is the central place for your custom development. It includes the following sub-directories:
|
||||
|
||||
- `admin`: Holds your admin dashboard's custom components and pages.
|
||||
- `api`: Holds your custom API routes that are added as endpoints in your Medusa application.
|
||||
- `jobs`: Holds your scheduled jobs that run at a specified interval during your Medusa application's runtime.
|
||||
- `modules`: Holds your custom modules that implement custom business logic.
|
||||
- `scripts`: Holds your custom scripts to be executed using Medusa's CLI tool.
|
||||
- `subscribers`: Holds your event listeners that are executed asynchronously whenever an event is emitted.
|
||||
- `workflows`: Holds your custom flows that can be executed from anywhere in your application.
|
||||
|
||||
## medusa-config.ts
|
||||
|
||||
This file holds your Medusa configurations, such as your PostgreSQL database configurations.
|
||||
@@ -0,0 +1,120 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Scheduled Jobs`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about scheduled jobs and how to use them.
|
||||
|
||||
## What is a Scheduled Job?
|
||||
|
||||
A scheduled job is a function executed at a specified interval of time in the background of your Medusa application. It’s like a cron job that runs during the application's runtime.
|
||||
|
||||
For example, you can synchronize your inventory with an Enterprise Resource Planning (ERP) system once a day using a scheduled job.
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Scheduled Job?
|
||||
|
||||
A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory.
|
||||
|
||||
For example, create the file `src/jobs/hello-world.ts` with the following content:
|
||||
|
||||
```ts title="src/jobs/hello-world.ts"
|
||||
// the scheduled-job function
|
||||
export default function () {
|
||||
console.log("Time to say hello world!")
|
||||
}
|
||||
|
||||
// the job's configurations
|
||||
export const config = {
|
||||
name: "every-minute-message",
|
||||
// execute every minute
|
||||
schedule: "* * * * *",
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
A scheduled job file must export:
|
||||
|
||||
- A function to be executed whenever it’s time to run the scheduled job.
|
||||
- A configuration object defining the job. It has two properties:
|
||||
- `name`: a unique name for the job.
|
||||
- `schedule`: a [cron expression](https://crontab.guru/) specifying when to run the job.
|
||||
|
||||
This scheduled job executes every minute and logs into the terminal `Time to say hello world!`.
|
||||
|
||||
### Test Scheduled Jobs
|
||||
|
||||
To test out your scheduled job, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
After a minute, the following message will be logged to the terminal:
|
||||
|
||||
```bash
|
||||
Time to say hello world!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use Scheduled Jobs
|
||||
|
||||
<Note title="Use scheduled jobs when" type="success">
|
||||
|
||||
- You're executing an action at a specified time interval during application runtime.
|
||||
- The action must be executed automatically.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Don't use scheduled jobs if" type="error">
|
||||
|
||||
- You want the action to execute at a specified time interval while the Medusa application **isn't** running. Instead, use the operating system's equivalent of a cron job.
|
||||
- You want to execute the action once. Use loaders instead.
|
||||
- You want to execute the action if an event occurs. Use subscribers instead.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Resolve Resources
|
||||
|
||||
The scheduled job function receives a `container` parameter, which is the Medusa container. Use it to resolve resources in your Medusa application, such as services.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["11", "resolve", "Resolve the Product Module's main service."],
|
||||
["11", "Modules.PRODUCT", "The module's registration name imported from `@medusajs/framework/utils`."]
|
||||
]
|
||||
|
||||
```ts title="src/jobs/hello-world.ts" highlights={highlights}
|
||||
import {
|
||||
IProductModuleService,
|
||||
MedusaContainer,
|
||||
} from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
export default async function myCustomJob(
|
||||
container: MedusaContainer
|
||||
) {
|
||||
const productModuleService: IProductModuleService =
|
||||
container.resolve(Modules.PRODUCT)
|
||||
|
||||
const [, count] = await productModuleService.listAndCountProducts()
|
||||
|
||||
console.log(
|
||||
`Time to check products! You have ${count} product(s)`
|
||||
)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
name: "every-minute-message",
|
||||
// execute every minute
|
||||
schedule: "* * * * *",
|
||||
}
|
||||
```
|
||||
|
||||
In the scheduled job function, you resolve the Product Module's main service and retrieve the number of products in the store, then log the number in the terminal.
|
||||
@@ -0,0 +1,270 @@
|
||||
import { CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Workflows`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about workflows and how to define and execute them.
|
||||
|
||||
## What is a Workflow?
|
||||
|
||||
A workflow is a series of queries and actions that complete a task.
|
||||
|
||||
You construct a workflow similar to how you create a JavaScript function, but unlike regular functions, a workflow creates an internal representation of your steps.
|
||||
|
||||
By using a workflow, you can track its execution's progress, provide roll-back logic for each step to mitigate data inconsistency when errors occur, automatically retry failing steps, and do much more, as explained in later chapters.
|
||||
|
||||
---
|
||||
|
||||
## How to Create and Execute a Workflow?
|
||||
|
||||
### 1. Create the Steps
|
||||
|
||||
A workflow is made of a series of steps. A step is created using the `createStep` utility function imported from `@medusajs/framework/workflows-sdk`.
|
||||
|
||||
Create the file `src/workflows/hello-world.ts` with the following content:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts"
|
||||
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
const step1 = createStep("step-1", async () => {
|
||||
return new StepResponse(`Hello from step one!`)
|
||||
})
|
||||
```
|
||||
|
||||
This creates one step that returns a hello message.
|
||||
|
||||
Steps can accept input parameters.
|
||||
|
||||
For example, add the following to `src/workflows/hello-world.ts`:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts"
|
||||
type WorkflowInput = {
|
||||
name: string
|
||||
}
|
||||
|
||||
const step2 = createStep("step-2", async ({ name }: WorkflowInput) => {
|
||||
return new StepResponse(`Hello ${name} from step two!`)
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Create a Workflow
|
||||
|
||||
Next, add the following to the same file to create the workflow using the `createWorkflow` function:
|
||||
|
||||
```ts title="src/workflows/hello-world.ts"
|
||||
import {
|
||||
// other imports...
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
// ...
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"hello-world",
|
||||
function (input: WorkflowInput) {
|
||||
const str1 = step1()
|
||||
// to pass input
|
||||
const str2 = step2(input)
|
||||
|
||||
return new WorkflowResponse({
|
||||
message: str1,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default myWorkflow
|
||||
```
|
||||
|
||||
This creates a `hello-world` workflow. When you create a workflow, it’s constructed but not executed yet.
|
||||
|
||||
The workflow must return an instance of `WorkflowResponse`, whose first parameter is returned to workflow executors.
|
||||
|
||||
### 3. Execute the Workflow
|
||||
|
||||
You can execute a workflow from different resources within Medusa.
|
||||
|
||||
- Use API routes to execute the workflow in response to an API request or a webhook.
|
||||
- Use subscribers to execute a workflow when an event is triggered.
|
||||
- Use scheduled jobs to execute a workflow on a regular schedule.
|
||||
|
||||
To execute the workflow, invoke it passing the Medusa container as a parameter. Then, use its `run` method:
|
||||
|
||||
<CodeTabs group="resource-types">
|
||||
<CodeTab label="API Route" value="api-route">
|
||||
|
||||
```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
|
||||
import type {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
import myWorkflow from "../../workflows/hello-world"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
) {
|
||||
const { result } = await myWorkflow(req.scope)
|
||||
.run({
|
||||
input: {
|
||||
name: req.query.name as string,
|
||||
},
|
||||
})
|
||||
|
||||
res.send(result)
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="Subscriber" value="subscriber">
|
||||
|
||||
```ts title="src/subscribers/customer-created.ts" highlights={[["20"], ["21"], ["22"], ["23"], ["24"], ["25"]]} collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
type SubscriberConfig,
|
||||
type SubscriberArgs,
|
||||
} from "@medusajs/framework"
|
||||
import myWorkflow from "../workflows/hello-world"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import { IUserModuleService } from "@medusajs/framework/types"
|
||||
|
||||
export default async function handleCustomerCreate({
|
||||
event: { data },
|
||||
container,
|
||||
}: SubscriberArgs<{ id: string }>) {
|
||||
const userId = data.id
|
||||
const userModuleService: IUserModuleService = container.resolve(
|
||||
Modules.USER
|
||||
)
|
||||
|
||||
const user = await userModuleService.retrieveUser(userId)
|
||||
|
||||
const { result } = await myWorkflow(container)
|
||||
.run({
|
||||
input: {
|
||||
name: user.first_name,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(result)
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: "user.created",
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="Scheduled Job" value="scheduled-job">
|
||||
|
||||
```ts title="src/jobs/message-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"]]}
|
||||
import { MedusaContainer } from "@medusajs/framework/types"
|
||||
import myWorkflow from "../workflows/hello-world"
|
||||
|
||||
export default async function myCustomJob(
|
||||
container: MedusaContainer
|
||||
) {
|
||||
const { result } = await myWorkflow(container)
|
||||
.run({
|
||||
input: {
|
||||
name: "John",
|
||||
},
|
||||
})
|
||||
|
||||
console.log(result.message)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
name: "run-once-a-day",
|
||||
schedule: `0 0 * * *`,
|
||||
};
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
### 4. Test Workflow
|
||||
|
||||
To test out your workflow, start your Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, send a `GET` request to `/workflow`:
|
||||
|
||||
```bash
|
||||
curl http://localhost:9000/workflow?name=john
|
||||
```
|
||||
|
||||
You’ll receive the following response:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Hello from step one!"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use Workflows
|
||||
|
||||
<Note title="Use workflows when" type="success">
|
||||
|
||||
You're implementing a custom feature exposed by an API route, or used in subscribers or scheduled jobs.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Resolve Resources
|
||||
|
||||
Each step in the workflow receives as a second parameter a `context` object. The object holds a `container` property which is the Medusa container. Use it to resolve other resources, such as services, of your Medusa application.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["12", "resolve", "Resolve the Product Module's main service."],
|
||||
[
|
||||
"12",
|
||||
"Modules.PRODUCT",
|
||||
"The resource registration name imported from `@medusajs/framework/utils`.",
|
||||
],
|
||||
]
|
||||
|
||||
```ts title="src/workflows/product-count.ts" highlights={highlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
|
||||
import {
|
||||
createStep,
|
||||
StepResponse,
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { IProductModuleService } from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
|
||||
const step1 = createStep("step-1", async (_, context) => {
|
||||
const productModuleService: IProductModuleService =
|
||||
context.container.resolve(Modules.PRODUCT)
|
||||
|
||||
const [, count] = await productModuleService.listAndCountProducts()
|
||||
|
||||
return new StepResponse(count)
|
||||
})
|
||||
|
||||
const myWorkflow = createWorkflow(
|
||||
"product-count",
|
||||
function () {
|
||||
const count = step1()
|
||||
|
||||
return new WorkflowResponse({
|
||||
count,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default myWorkflow
|
||||
```
|
||||
|
||||
In the step, you resolve the Product Module's main service and use it to retrieve the product count.
|
||||
Reference in New Issue
Block a user