docs: add documentation for v1.8 (#3669)

This commit is contained in:
Shahed Nasser authored and GitHub committed 2023-04-03 13:50:59 +02:00
1 parent 0cca13779d
commit c6bfad14d8
123 files changed
+7610 -2697

No files matched your search

@@ -1,5 +1,5 @@
---
description: 'Learn about the different configurations available in a Medusa backend. This includes configurations related to the database, CORS, plugins, redis, and more.'
description: 'Learn about the different configurations available in a Medusa backend. This includes configurations related to the database, CORS, plugins, and more.'
---
# Configure Medusa Backend
@@ -8,32 +8,18 @@ In this document, you’ll learn what configurations you can add to your Medusa
## Prerequisites
This document assumes you already followed along with the [“Set up your development environment” documentation](./prepare-environment.mdx) and have [installed a Medusa backend](./install.mdx#create-a-medusa-backend).
This document assumes you already followed along with the [Prepare Environment documentation](./prepare-environment.mdx) and have [installed a Medusa backend](./install.mdx#create-a-medusa-backend).
---
## Medusa Configurations File
The configurations for your Medusa backend are in `medusa-config.js`. This includes database, Redis, and plugin configurations, among other configurations.
The configurations for your Medusa backend are in `medusa-config.js`. This includes database, modules, and plugin configurations, among other configurations.
Some of the configurations mentioned in this document are already defined in `medusa-config.js` with default values. It’s important that you know what these configurations are used for and how to set them.
---
## Environment Variables
In your configurations, you’ll often use environment variables. For example, when using API keys or setting your database URL.
By default, Medusa loads environment variables from the system’s environment variables. Any different method you prefer to use or other location you’d prefer to load environment variables from you need to manually implement.
:::info
This change in how environment variables are loaded was introduced in version 1.3.0. You can learn more in the [upgrade guide for version 1.3.0](../../upgrade-guides/medusa-core/1-3-0.md).
:::
---
## Database Configuration
Medusa supports two database types: SQLite and PostgreSQL.
@@ -131,9 +117,15 @@ module.exports = {
## Redis
Medusa uses Redis to handle the event queue, among other usages. You need to set Redis URL in the configurations:
:::note
```jsx
As of v1.8 of the Medusa core package, Redis is only used for scheduled jobs. For events handling, it's now moved into a module and can be configured as explained [here](#modules).
:::
You must first have Redis installed. You can refer to [Redis's installation guide](https://redis.io/docs/getting-started/installation/). You need to set Redis URL in the configurations:
```js
module.exports = {
projectConfig: {
// ...other configurations
@@ -144,7 +136,7 @@ module.exports = {
Where `REDIS_URL` is the URL used to connect to Redis. The format of the connection string is `redis[s]://[[username][:password]@][host][:port][/db-number]`.
If you omit this configuration, events will not be emitted and subscribers will not work.
If you omit this configuration, scheduled jobs will not work.
:::tip
@@ -160,12 +152,6 @@ REDIS_URL=<YOUR_REDIS_URL>
Where `<YOUR_REDIS_URL>` is the URL of your Redis backend.
:::info
You can learn more about Subscribers and events in the [Subscriber documentation](../events/create-subscriber.md).
:::
---
## JWT Secret
@@ -203,7 +189,7 @@ In a development environment, if this option is not set the default secret is
This configuration is used to sign the session ID cookie. To set the cookie secret:
```jsx
```js
module.exports = {
projectConfig: {
// ...other configurations
@@ -273,7 +259,7 @@ The examples above apply to both Admin and Store CORS.
To make sure your Admin dashboard can access the Medusa backend’s admin endpoints, set this configuration:
```jsx
```js
module.exports = {
projectConfig: {
// ...other configurations
@@ -302,7 +288,7 @@ Make sure that the URL is without a backslash at the end. For example, you shoul
To make sure your Storefront dashboard can access the Medusa backend, set this configuration:
```jsx
```js
module.exports = {
projectConfig: {
// ...other configurations
@@ -331,7 +317,7 @@ Make sure that the URL is without a backslash at the end. For example, you shoul
## Plugins
On your Medusa backend, you can use Plugins to add custom features or integrate third-party services. For example, installing a plugin to use Stripe as a payment provider.
On your Medusa backend, you can use Plugins to add custom features or integrate third-party services. For example, installing a plugin to use Stripe as a payment processor.
:::info
@@ -385,7 +371,61 @@ It is recommended to use environment variables to store values of options instea
---
## Modules
In Medusa, commerce and core logic are modularized to allow developers to extend or replace certain modules with custom implementations.
:::tip
You can learn more about Modules in the [Modules Overview documentation](../modules/overview.mdx).
:::
Aside from installing the module with NPM, you need to add into the exported object in `medusa-config.js`. For example:
```js
module.exports = {
// ...
modules: {
// ...
moduleType: {
resolve: "<module_name>",
options: {
// options if necessary
},
},
},
}
```
`moduleType` and `<module_name>` are just placeholder that should be replaced based on the type of Module you're adding. For example, if you used the default Medusa starter to create your backend, you should have an `eventBus` module installed:
```js
module.exports = {
// ...
modules: {
// ...
eventBus: {
resolve: "@medusajs/event-bus-local",
},
},
}
```
Each module can have its own options. You must refer to the module's documentation to learn about its options.
### Recommended Event Bus Modules
In the default Medusa starter, the local event bus module is used. This module is good for testing out Medusa and during development, but it's highly recommended to use the [Redis event module](../events/modules/redis.md) instead for better development experience and during production.
### Recommended Cache Modules
In the default Medusa starter, the in-memory cache module is used. This module is good for testing out Medusa and during development, but it's highly recommended to use the [Redis cache module](../cache/modules/redis.md) instead for better development experience and during production.
---
## See Also
- [Medusa architecture overview](../fundamentals/architecture-overview.md)
- [Plugins](../plugins/overview.mdx)
- [Plugins](../plugins/overview.mdx)
- [Modules](../modules/overview.mdx)
+1 -1
View File
@@ -85,7 +85,7 @@ Medusa backend is made up of different resources that make it a powerful server.
### Set Up Development Environment
For an optimal experience developing with Medusa and to make sure you can use its advanced functionalities, you'll need to install more tools such as Redis or PostgreSQL.
For an optimal experience developing with Medusa and to make sure you can use its advanced functionalities, you'll need to install more tools such as PostgreSQL.
Follow [this documentation to learn how to set up your development environment](../../development/backend/prepare-environment.mdx).
@@ -1,5 +1,5 @@
---
description: 'Learn how to prepare your development environment while using Medusa. This guide includes how to install Node.js, Git, Medusa CLI tool, PostgreSQL, and Redis.'
description: 'Learn how to prepare your development environment while using Medusa. This guide includes how to install Node.js, Git, Medusa CLI tool, and PostgreSQL.'
---
import styles from './development.module.css';
@@ -213,73 +213,6 @@ Where:
</TabItem>
</Tabs>
### Redis
Medusa uses Redis as the event queue in the backend. If you want to use subscribers to handle events such as when an order is placed and perform actions based on the events, then you need to install and configure Redis.
If you don’t install and configure Redis with your Medusa backend, then it will work without any events-related features.
:::tip
After installing Redis, check out the [Configure your Backend documentation](./configurations.md#redis) to learn how to configure Redis to work with Medusa.
:::
<Tabs groupId="operating-systems" wrapperClassName={styles.osTabs}>
<TabItem value="windows" label="Windows">
To use Redis on Windows, you must have [Windows Subsystem for Linux (WSL2) enabled](https://docs.microsoft.com/en-us/windows/wsl/install). This lets you run Linux binaries on Windows.
After installing and enabling WSL2, if you use an Ubuntu distribution you can run the following commands to install Redis:
```bash
sudo apt-add-repository ppa:redislabs/redis
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install redis-server
## Start Redis server
sudo service redis-server start
```
</TabItem>
<TabItem value="linux" label="Linux">
If you use Ubuntu you can use the following commands to install Redis:
```bash
curl -fsSL https://packages.redis.io/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" \
| sudo tee /etc/apt/sources.list.d/redis.list
sudo apt-get update
sudo apt-get install redis
## Start Redis server
sudo service redis-server start
```
For other distributions, you can check out [Redis’ guide on this](https://redis.io/docs/getting-started/installation/install-redis-on-linux/).
</TabItem>
<TabItem value="macos" label="macOS">
You can install Redis on macOS using Homebrew with the following command:
```bash
brew install redis
## Start Redis server
brew services start redis
```
To install Redis without Homebrew you can check out [Redis’s guide on installing it from source](https://redis.io/docs/getting-started/installation/install-redis-from-source/).
</TabItem>
</Tabs>
---
## See Also
@@ -28,11 +28,7 @@ This documentation helps you learn how to create a batch job strategy. The batch
### Medusa Components
It is assumed that you already have a Medusa backend installed and set up. If not, you can follow our [quickstart guide](../backend/install.mdx) to get started.
### Redis
Redis is required for batch jobs to work. Make sure you [install Redis](../backend/prepare-environment.mdx#redis) and [configure it with your Medusa backend](../backend/configurations.md#redis).
It is assumed that you already have a Medusa backend installed and set up. If not, you can follow our [quickstart guide](../backend/install.mdx) to get started. The Medusa backend must also have an event bus module installed, which is available when using the default Medusa backend starter.
### PostgreSQL
@@ -19,11 +19,7 @@ Although this documentation specifically targets import strategies, you can use
### Medusa Components
It's assumed that you already have a Medusa backend installed and set up. If not, you can follow our [quickstart guide](../backend/install.mdx) to get started.
### Redis
Redis is required for batch jobs to work. Make sure you [install Redis](../backend/prepare-environment.mdx#redis) and [configure it with your Medusa backend](../backend/configurations.md#redis).
It's assumed that you already have a Medusa backend installed and set up. If not, you can follow our [quickstart guide](../backend/install.mdx) to get started. The Medusa backend must also have an event bus module installed, which is available when using the default Medusa backend starter.
### PostgreSQL
@@ -13,7 +13,7 @@ In this document, you’ll learn what Batch Jobs are and how they work in Medusa
Batch Jobs are tasks that can be performed asynchronously and iteratively. They can be [created using the Admin API](/api/admin/#tag/Batch-Job/operation/PostBatchJobs), then, once confirmed, they are processed asynchronously.
Batch jobs require Redis, which Medusa uses as a queuing system to register and handle events. Every status change of a batch job triggers an event that can be handled using [subscribers](../events/subscribers.mdx).
Every status change of a batch job triggers an event that can be handled using [subscribers](../events/subscribers.mdx).
Medusa uses batch jobs in its core to perform some asynchronous tasks. For example, the Export Products functionality uses batch jobs.
+265
View File
@@ -0,0 +1,265 @@
---
description: 'In this document, you’ll learn about how to create a cache module in Medusa, using Memcached as an example.'
---
# How to Create a Cache Module
In this document, you will learn how to build your own Medusa cache module.
## Overview
Medusa provides ready-made modules for cache, including in-memory and Redis modules. If you prefer another technology used for caching in your commerce application, you can build a module locally and use it in your Medusa backend. You can also publish to NPM and reuse it across multiple Medusa backend instances.
In this document, you will learn how to build your own Medusa cache module based on Memcached as an example. This gives you a real-life example of creating the cache module. You can follow the general steps with any other caching system or service.
---
## Prerequisites
If you want to create the Memcached cache module as explained in this guide, you must have [Memcached](https://memcached.org/) installed and running.
---
## (Optional) Step 0: Prepare Module Directory
Before you start implementing your module, it's recommended to prepare the directory or project holding your custom implementation.
You can refer to the [Project Preparation step in the Create Module documentation](../modules/create.mdx#optional-step-0-project-preparation) to learn how to do that.
---
## Step 1: Create the Service
Create the file `services/memcached-cache.ts` which will hold your cache service. Note that the name of the file is recommended to be in the format `<service_name>-cache`. So, if you’re not integrating `memcached`, you should replace the name with what’s relevant for your module.
Add the following content to the file:
```ts title=services/memcached-cache.ts
import { ICacheService } from "@medusajs/types"
class MemcachedCacheService implements ICacheService {
async get<T>(key: string): Promise<T> {
throw new Error("Method not implemented.")
}
async set(
key: string,
data: unknown,
ttl?: number
): Promise<void> {
throw new Error("Method not implemented.")
}
async invalidate(key: string): Promise<void> {
throw new Error("Method not implemented.")
}
}
export default MemcachedCacheService
```
This creates the class `MemcachedCacheService` that implements the `ICacheService` interface imported from `@medusajs/types`. Feel free to rename the class to what’s relevant for your cache service.
In the class, you must implement three methods: `get`, `set`, and `invalidate`. You’ll learn what each of these methods do in the upcoming section.
---
## Step 2: Implement the Methods
### constructor
The `constructor` method of a service allows you to prepare any third-party client or service necessary to be used in other methods. It also allows you to get access to the module’s options which are typically defined in `medusa-config.js`, and to other services and resources in the Medusa backend using [dependency injection](../fundamentals/dependency-injection.md).
Here’s an example of how you can use the `constructor` to create a memcached instance and save the module’s options:
```ts title=src/services/memcached-cache.ts
import { ICacheService } from "@medusajs/types"
import Memcached from "memcached"
const DEFAULT_CACHE_TIME = 30
export type MemcachedCacheModuleOptions = {
/**
* Time to keep data in the cache (in seconds)
*/
ttl?: number
/**
* Allow passing the configuration for Memcached client
*/
location: Memcached.Location
options?: Memcached.options
}
class MemcachedCacheService implements ICacheService {
protected readonly memcached: Memcached
protected readonly TTL: number
constructor(
{
// inject services through dependency injection
// for example you can access the logger
logger,
},
options: MemcachedCacheModuleOptions
) {
this.memcached = new Memcached(
options.location,
options.options
)
this.TTL = options.ttl || DEFAULT_CACHE_TIME
}
// ...
}
```
### get
The `get` method allows you to retrieve the value of a cached item based on its key. The method accepts a string as a first parameter, which is the key in the cache. It either returns the cached item or `null` if it doesn’t exist.
Here’s an example implementation of this method for a Memcached service:
```ts title=src/services/memcached-cache.ts
class MemcachedCacheService implements ICacheService {
// ...
async get<T>(cacheKey: string): Promise<T | null> {
return new Promise((res, rej) => {
this.memcached.get(cacheKey, (err, data) => {
if (err) {
res(null)
} else {
if (data) {
res(JSON.parse(data))
} else {
res(null)
}
}
})
})
}
}
```
### set
The `set` method is used to set an item in the cache. It accepts three parameters:
1. The first parameter `key` is a string that represents the key of the data being added to the cache. This key can be used later to get or invalidate the cached item.
2. The second parameter `data` is the data to be added to the cache. There’s no defined type for this data.
3. The third parameter `ttl` is optional. It’s a number indicating how long (in seconds) the data should be kept in the cache.
Here’s an example of an implementation of this method for a Memcached service:
```ts title=src/services/memcached-cache.ts
class MemcachedCacheService implements ICacheService {
// ...
async set(
key: string,
data: Record<string, unknown>,
ttl: number = this.TTL
): Promise<void> {
return new Promise((res, rej) =>
this.memcached.set(
key, JSON.stringify(data), ttl, (err) => {
if (err) {
rej(err)
} else {
res()
}
})
)
}
}
```
### invalidate
The `invalidate` method removes an item from the cache using its key. By default, the item is removed from the cache when the time-to-live (ttl) expires. The `invalidate` method can be used to remove the item beforehand.
The method accepts a string as a first parameter, which is the key of the item to invalidate and remove from the cache.
Here’s an example of an implementation of this method for a Memcached service:
```ts title=src/services/memcached-cache.ts
class MemcachedCacheService implements ICacheService {
// ...
async invalidate(key: string): Promise<void> {
return new Promise((res, rej) => {
this.memcached.del(key, (err) => {
if (err) {
rej(err)
} else {
res()
}
})
})
}
}
```
---
## Step 3: Export the Service
After implementing the cache service, you must export it so that the Medusa backend can use it.
Create the file `index.ts` with the following content:
```ts title=index.ts
import { ModuleExports } from "@medusajs/modules-sdk"
import { MemcachedCacheService } from "./services"
const service = MemcachedCacheService
const moduleDefinition: ModuleExports = {
service,
}
export default moduleDefinition
```
This exports a module definition, which requires at least a `service`. If you named your service something other than `MemcachedCacheService`, make sure to replace it with that.
You can learn more about what other properties you can export in your module definition in the [Create a Module documentation](../modules/create.mdx#step-2-export-module).
---
## Step 4: Test your Module
You can test your module in the Medusa backend by referencing it in the configurations.
To do that, add the module to the exported configuration in `medusa-config.js` as follows:
```js title=medusa-config.js
module.exports = {
// ...
modules: {
// ...
cacheService: {
resolve: "path/to/custom-module",
options: {
// any necessary options
ttl: 30,
location: "localhost:55000",
},
},
},
}
```
Make sure to replace the `path/to/custom-module` with a relative path from your Medusa backend to your module. You can learn more about module reference in the [Create Module documentation](../modules/create.mdx#module-reference).
You can also add any necessary options to the module. The options added in the example above are relevant to the memcached module and you can replace them with your own options.
Then, to test the module, run the Medusa backend which also runs your module:
```bash npm2yarn
npm run start
```
---
## (Optional) Step 5: Publish your Module
You can publish your cache module to NPM. This can be useful if you want to reuse your module across Medusa backend instances, or want to allow other developers to use it.
You can refer to the [Publish Module documentation](../modules/publish.md) to learn how to publish your module.
+70
View File
@@ -0,0 +1,70 @@
---
description: 'In this document, you’ll learn about the in-memory cache module and how you can install it in your Medusa backend.'
---
# In-Memory Cache Module
In this document, you’ll learn about the in-memory cache module and how you can install it in your Medusa backend.
## Overview
Medusa’s modular architecture allows developers to extend or completely replace the logic used for caching. You can create a custom module, or you can use the modules Medusa provides.
Medusa’s default starter project uses the in-memory cache module. The in-memory cache module uses a plain JavaScript Map object to store the cache data.
This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production. For production, it’s recommended to use modules like [Redis Cache Module](./redis.md).
This document will guide you through installing the in-memory cache module.
---
## Prerequisites
### Medusa Backend
It’s assumed you already have a Medusa backend installed. If not, you can learn how to install it by following [this guide](../../backend/install.mdx).
---
## Step 1: Install the Module
In the root directory of your Medusa backend, install the in-memory cache module with the following command:
```bash npm2yarn
npm install @medusajs/cache-inmemory
```
---
## Step 2: Add Configuration
In `medusa-config.js`, add the following to the exported object:
```js title=medusa-config.js
module.exports = {
// ...
modules: {
// ...
cacheService: {
resolve: "@medusajs/cache-inmemory",
options: {
ttl: 30,
},
},
},
}
```
This registers the in-memory cache module as the main cache service to use. You pass it the option `ttl`. This means time-to-live, and it indicates the number of seconds an item can live in the cache before it’s removed.
---
## Step 3: Test Module
To test the module, run the following command to start the Medusa backend:
```bash npm2yarn
npm run start
```
The backend should then start with no errors, indicating that the module was installed successfully.
+92
View File
@@ -0,0 +1,92 @@
---
description: 'In this document, you’ll learn about the Redis cache module and how you can install it in your Medusa backend.'
---
# Redis Cache Module
In this document, you’ll learn about the Redis cache module and how you can install it in your Medusa backend.
## Overview
Medusa’s modular architecture allows developers to extend or replace the logic used for [caching](../overview.mdx). You can create a custom module, or you can use the modules Medusa provides.
One of these modules is the Redis module. This module allows you to utilize Redis for the caching functionality. This document will you guide you through installing the Redis module.
---
## Prerequisites
### Medusa Backend
It’s assumed you already have a Medusa backend installed. If not, you can learn how to install it by following [this guide](../../backend/install.mdx).
### Redis
You must have Redis installed and configured in your Medusa backend. You can learn how to install it from the [Redis documentation](https://redis.io/docs/getting-started/installation/).
---
## Step 1: Install the Module
In the root directory of your Medusa backend, install the Redis cache module with the following command:
```bash npm2yarn
npm install @medusajs/cache-redis
```
---
## Step 2: Add Environment Variable
The Redis cache module requires a connection URL to Redis as part of its options. If you don’t already have an environment variable set for a Redis URL, make sure to add one:
```bash
CACHE_REDIS_URL=<YOUR_REDIS_URL>
```
Where `<YOUR_REDIS_URL>` is a connection URL to your Redis instance.
---
## Step 3: Add Configuration
In `medusa-config.js`, add the following to the exported object:
```js title=medusa-config.js
module.exports = {
// ...
modules: {
// ...
cacheService: {
resolve: "@medusajs/cache-redis",
options: {
redisUrl: process.env.CACHE_REDIS_URL,
ttl: 30,
},
},
},
}
```
This registers the Redis cache module as the main cache service to use. In the options, you pass `redisUrl` with the value being the environment variable you set. You also pass the option `ttl`. This means time-to-live, and it indicates the number of seconds an item can live in the cache before it’s removed.
Other available options include:
- `redisOptions`: an object containing options for the Redis instance. You can learn about available options in [io-redis’s documentation](https://luin.github.io/ioredis/index.html#RedisOptions). By default, it’s an empty object.
- `namespace`: a string used to prefix event keys. By default, it's `medusa`.
---
## Step 4: Test Module
To test the module, run the following command to start the Medusa backend:
```bash npm2yarn
npm run start
```
If the module was installed successfully, you should see the following message in the logs:
```bash noCopy noReport
Connection to Redis in module 'cache-redis' established
```
+65
View File
@@ -0,0 +1,65 @@
---
description: 'In this document, you’ll learn about Cache Modules in Medusa, how they work, and which modules Medusa provides.'
---
import DocCardList from '@theme/DocCardList';
import DocCard from '@theme/DocCard';
import Icons from '@theme/Icon';
# Cache
In this document, you’ll learn about Cache Modules in Medusa, how they work, and which modules Medusa provides.
## Overview
Cache is used in Medusa to store the results of computations such as price selection or various tax calculations. The caching layer in Medusa is implemented as a [module](../modules/overview.mdx). This allows you to replace the underlying database/cache store in your project.
Modules are packages that export a service. The main service exported from a cache module has to implement the `ICacheService` interface exported from the `@medusajs/types` package.
During the app startup, the modules loader will load the service into the [dependency injection container](../fundamentals/dependency-injection.md) to be available as `cacheService` to the rest of the commerce application.
---
## Available Modules
Medusa default starter project comes with the in-memory cache module (`@medusajs/cache-inmemory`). In-memory cache uses a plain JS Map object to store data, which is great for testing and development purposes.
For production environments, there is the Redis cache module package (`@medusajs/cache-redis`) that you can install.
<DocCardList colSize={6} items={[
{
type: 'link',
href: '/development/cache/modules/redis',
label: 'Redis Module',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to install and use the Redis Module'
}
},
{
type: 'link',
href: '/development/cache/modules/in-memory',
label: 'In-Memory Module',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to install and use the In-Memory Module'
}
},
]} />
---
## Custom Development
Developers can create custom cache modules to implement their custom cache logic. The module can be installed and used in any Medusa backend.
<DocCard item={{
type: 'link',
href: '/development/cache/create',
label: 'Create a Cache Module',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to create a cache module in Medusa.'
}
}}
/>
@@ -3,7 +3,7 @@ description: 'Learn how to add a middleware in Medusa. A middleware is a functio
addHowToData: true
---
# How to Add a Middleware
# Middlewares
In this document, you’ll learn how to add a middleware to an existing or custom route in Medusa.
@@ -19,36 +19,17 @@ You can add a middleware to an existing route in the Medusa backend, a route in
---
## Add a Middleware
## How to Add a Middleware
Adding a middleware is very similar to adding a custom endpoint. The middleware must be created either in the `src/api/index.ts` entry point, or other TypeScript or JavaScript files imported into `src/api/index.ts`.
### Step 1: Create the Middleware File
:::info
You can organize your middlewares as you see fit, but it's recommended to create Middlewares in the `src/api/middlewares` directory. It's recommended to create each middleware in a different file.
Learn more about creating custom endpoints in [this documentation](./create.md).
Each file should export a middleware function that accepts three parameters:
:::
The following code snippet is an example of adding a middleware:
```ts title=src/api/index.ts
import { Router } from "express"
export default () => {
const router = Router()
router.use("/store/products", (req, res, next) => {
// perform an action when retrieving products
next()
})
return router
}
```
This code snippet adds a middleware to the [List Products](/api/store/#tag/Product/operation/GetProducts) endpoint. In the middleware function, you can perform any action.
Then, you must call the `next` method received as a third parameter in the middleware function to ensure that the endpoint executes after the middleware.
1. The first one is an Express request object. It can be used to get details related to the request or resolve resources from the dependency container.
2. The second one is an Express response object. It can be used to modify the response, or in some cases return a response without executing the associated endpoint.
3. The third one is a next middleware function that ensures that other middlewares and the associated endpoint are executed.
:::info
@@ -56,9 +37,53 @@ You can learn more about Middlewares and their capabilities in [Express’s docu
:::
---
Here's an example of a middleware:
## Building Files
```ts title=src/api/middlewares/custom-middleware.ts
export function customMiddleware(req, res, next) {
// TODO perform an action
next()
}
```
### Step 2: Apply Middleware on an Endpoint
To apply a middleware on any endpoint, you can use the same router defined in `src/api/index.ts` or any other router that is used or exported by `src/api/index.ts`. For example:
:::warning
The examples used here don't apply Cross-Origin Resource Origin (CORS) options for simplicity. Make sure to apply them, especially for core routes, as explained in the [Create Endpoint](./create.md#cors-configuration) documentation.
:::
```ts title=src/api/index.ts
import { Router } from "express"
import {
customMiddleware,
} from "./middlewares/custom-middleware"
export default (rootDirectory, pluginOptions) => {
const router = Router()
// custom route
router.get("/hello", (req, res) => {
res.json({
message: "Welcome to My Store!",
})
})
// middleware for the custom route
router.use("/hello", customMiddleware)
// middleware for core route
router.use("/store/products", customMiddleware)
return router
}
```
## Step 3: Building Files
Similar to custom endpoints, you must transpile the files under `src` into the `dist` directory for the backend to load them.
@@ -68,6 +93,100 @@ To do that, run the following command before running the Medusa backend:
npm run build
```
You can then test that the middleware is working by running the backend.
---
## Registering New Resources in Dependency Container
In some cases, you may need to register a resource to use within your commerce application. For example, you may want to register the logged-in user to access it in other services. You can do that in your middleware.
:::tip
If you want to register a logged-in user and access it in your resources, you can check out [this example guide](./example-logged-in-user.md).
:::
To register a new resource in the dependency container, use the `req.scope.register` method:
```ts title=src/api/middlewares/custom-middleware.ts
export function customMiddleware(req, res, next) {
// TODO perform an action
req.scope.register({
customResource: {
resolve: () => "my custom resource",
},
})
next()
}
```
You can then load this new resource within other resources. For example, to load it in a service:
<!-- eslint-disable prefer-rest-params -->
```ts title=src/services/custom-service.ts
import { TransactionBaseService } from "@medusajs/medusa"
class CustomService extends TransactionBaseService {
constructor(container, options) {
super(...arguments)
// use the registered resource.
try {
container.customResource
} catch (e) {
// avoid errors when the backend first loads
}
}
}
export default CustomService
```
Notice that you have to wrap your usage of the new resource in a try-catch block when you use it in a constructor. This is to avoid errors that can arise when the backend first loads, as the resource is not registered yet.
### Note About Services Lifetime
If you want to access new registrations in the dependency container within a service, you must set the lifetime of the service either to `Lifetime.SCOPED` or `Lifetime.TRANSIENT`. Services that have a `Lifetime.SINGLETON` lifetime can't access new registrations since they're resolved and cached in the root dependency container beforehand. You can learn more in the [Create Services documentation](../services/create-service.md#service-life-time).
For custom services, no additional action is required as the default lifetime is `Lifetime.TRANSIENT`. However, if you extend a core service, you must change the lifetime since the default lifetime for core services is `Lifetime.SINGLETON`.
For example:
<!-- eslint-disable prefer-rest-params -->
```ts
import { Lifetime } from "awilix"
import {
ProductService as MedusaProductService,
} from "@medusajs/medusa"
// extending ProductService from the core
class ProductService extends MedusaProductService {
// The default life time for a core service is SINGLETON
static LIFE_TIME = Lifetime.SCOPED
constructor(container, options) {
super(...arguments)
// use the registered resource.
try {
container.customResource
} catch (e) {
// avoid errors when the backend first loads
}
}
// ...
}
export default ProductService
```
---
## See Also
@@ -0,0 +1,193 @@
---
description: 'In this document, you’ll see an example of how you can use middlewares and endpoints to register the logged-in user in the dependency container of your commerce application.'
addHowToData: true
---
# Example: Access Logged-In User
In this document, you’ll see an example of how you can use middlewares and endpoints to register the logged-in user in the dependency container of your commerce application. You can then access the logged-in user in other resources, such as services.
This guide showcases how to register the logged-in admin user, but you can apply the same steps if you want to register the current customer.
This documentation does not explain the basics of [middlewares](./add-middleware.md) and [endpoints](./create.md). You can refer to their respective guides for more details about each.
## Step 1: Create the Middleware
Create the file `src/api/middlewareds/logged-in-user.ts` with the following content:
```ts title=src/api/middlewareds/logged-in-user.ts
import { User, UserService } from "@medusajs/medusa"
export async function registerLoggedInUser(req, res, next) {
let loggedInUser: User | null = null
if (req.user && req.user.userId) {
const userService =
req.scope.resolve("userService") as UserService
loggedInUser = await userService.retrieve(req.user.userId)
}
req.scope.register({
loggedInUser: {
resolve: () => loggedInUser,
},
})
next()
}
```
This retrieves the ID of the current user to retrieve an instance of it, then registers it in the scope under the name `loggedInUser`.
---
## Step 2: Apply Middleware on Endpoint
Create the file `src/api/routes/create-product.ts` with the following content:
```ts title=src/api/routes/create-product.ts
import cors from "cors"
import { Router } from "express"
import {
registerLoggedInUser,
} from "../middlewares/logged-in-user"
import
authenticate
from "@medusajs/medusa/dist/api/middlewares/authenticate"
const router = Router()
export default function (adminCorsOptions) {
// This router will be applied before the core routes.
// Therefore, the middleware will be executed
// before the create product handler is hit
router.use(
"/admin/products",
cors(adminCorsOptions),
authenticate(),
registerLoggedInUser
)
return router
}
```
In the example above, the middleware is applied on the `/admin/products` core endpoint. However, you can apply it on any other endpoint. You can also apply it to custom endpoints.
For endpoints that require Cross-Origin Resource Origin (CORS) options, such as core endpoints, you must pass the CORS options to the middleware as well since it will be executed before the underlying endpoint.
:::tip
In the above code snippet, the `authenticate` middleware imported from `@medusajs/medusa` is used to ensure that the user is logged in first. If you're implementing this for middleware to register the logged-in customer, make sure to use the [customer's authenticate middleware](./create.md#protect-store-routes).
:::
---
## Step 3: Register Endpoint in the API
Create the file `src/api/index.ts` with the following content:
```ts title=src/api/index.ts
import configLoader from "@medusajs/medusa/dist/loaders/config"
import createProductRouter from "./routes/create-product"
export default async function (rootDirectory: string) {
const config = await configLoader(rootDirectory)
const adminCors = {
origin: config.projectConfig.admin_cors.split(","),
credentials: true,
}
const productRouters = [
createProductRouter(adminCors),
]
return [...productRouters]
}
```
This exports an array of endpoints, one of them being the product endpoint that you applied the middleware on in the second step. You can export more endpoints as well.
---
## Step 4: Use in a Service
You can now access the logged-in user in a service. For example, to access it in a custom service:
<!-- eslint-disable prefer-rest-params -->
```ts
import { Lifetime } from "awilix"
import {
TransactionBaseService,
User,
} from "@medusajs/medusa"
class HelloService extends TransactionBaseService {
protected readonly loggedInUser_: User | null
constructor(container, options) {
super(...arguments)
try {
this.loggedInUser_ = container.loggedInUser
} catch (e) {
// avoid errors when backend first runs
}
}
// ...
}
export default HelloService
```
If you're accessing it in an extended core service, it’s important to change the lifetime of the service to `Lifetime.SCOPED`. For example:
<!-- eslint-disable prefer-rest-params -->
```ts
import { Lifetime } from "awilix"
import {
ProductService as MedusaProductService,
User,
} from "@medusajs/medusa"
// extend core product service
class ProductService extends MedusaProductService {
// The default life time for a core service is SINGLETON
static LIFE_TIME = Lifetime.SCOPED
protected readonly loggedInUser_: User | null
constructor(container, options) {
super(...arguments)
this.loggedInUser_ = container.loggedInUser
}
}
export default ProductService
```
You can learn more about the importance of changing the service lifetime in the [Middlewares documentation](./add-middleware.md#note-about-services-lifetime).
---
## Step 5: Test it Out
To test out your implementation, run the following command in the root directory of the Medusa backend to transpile your changes:
```bash npm2yarn
npm run build
```
Then, run your backend with the following command:
```bash npm2yarn
npm run start
```
If you try accessing the endpoints you added the middleware to, you should see your implementation working as expected.
+62 -25
View File
@@ -3,11 +3,11 @@ description: 'Learn how to create an entity in Medusa. This guide also explains
addHowToData: true
---
# Create an Entity
# How to Create an Entity
In this document, you’ll learn how you can create an [Entity](./overview.mdx).
In this document, you’ll learn how you can create a custom [Entity](./overview.mdx).
## Create the Entity
## Step 1: Create the Entity
To create an entity, create a TypeScript file in `src/models`. For example, here’s a `Post` entity defined in the file `src/models/post.ts`:
@@ -52,46 +52,85 @@ export class Post extends SoftDeletableEntity {
You can learn more about what decorators and column types you can use in [Typeorm’s documentation](https://typeorm.io/entities).
### Create a Migration
---
## Step 2: Create a Migration
Additionally, you must create a migration for your entity. Migrations are used to update the database schema with new tables or changes to existing tables.
You can learn more about Migrations, how to create them, and how to run them in the [Migration documentation](./migrations/overview.mdx).
You can learn more about Migrations, how to create or generate them, and how to run them in the [Migration documentation](./migrations/overview.mdx).
### Create a Repository
---
## Step 3: Create a Repository
Entities data can be easily accessed and modified using Typeorm [Repositories](https://typeorm.io/working-with-repository). To create a repository, create a file in `src/repositories`. For example, here’s a repository `PostRepository` created in `src/repositories/post.ts`:
```ts title=src/repositories/post.ts
import { EntityRepository, Repository } from "typeorm"
import { Post } from "../models/post"
import {
dataSource,
} from "@medusajs/medusa/dist/loaders/database"
@EntityRepository(Post)
export class PostRepository extends Repository<Post> { }
export const PostRepository = dataSource
.getRepository(Post)
export default PostRepository
```
This repository is created for the `Post` and that is indicated using the decorator `@EntityRepository`.
The repository is created using the `getRepository` method of the data source exported from the core package in Medusa. This method accepts the entity as a parameter.
:::tip
Be careful with your file names as it can cause unclear errors in Typeorm. Make sure all your file names are small letters for both entities and repositories to avoid any issues with file names.
A data source is Typeorm’s connection settings that allows you to connect to your database. You can learn more about it in [Typeorm’s documentation](https://typeorm.io/data-source).
:::
If you want to add methods to that repository or override Typeorm's Repository methods, you can do that using the `extend` method:
```ts title=src/repositories/post.ts
import { Post } from "../models/post"
import {
dataSource,
} from "@medusajs/medusa/dist/loaders/database"
export const PostRepository = dataSource
.getRepository(Post)
.extend({
customFunction(): void {
// TODO add custom implementation
return
},
})
export default PostRepository
```
You can learn about available Repository methods in [Typeorm's documentation](https://typeorm.io/repository-api).
---
## Access a Custom Entity
## Step 4: Run Migrations
:::note
Before you start using your entity, make sure to run the migrations that reflect the entity on your database schema.
Before trying this step make sure that you’ve created and run your migrations. You also need to re-build your code using:
To do that, run the `build` command that transpiles your code:
```bash npm2yarn
npm run build
```
:::
Then, run the `migration` command:
```bash npm2yarn
medusa migrations run
```
You should see that your migration have executed.
---
## Step 5: Use Your Entity
You can access your custom entity data in the database in services or subscribers using the repository. For example, here’s a service that lists all posts:
@@ -107,9 +146,9 @@ class PostService extends TransactionBaseService {
}
async list() {
const postRepository = this.manager_
.getCustomRepository(this.postRepository)
return await postRepository.find()
const postRepo = this.manager_
.withRepository(this.postRepository)
return await postRepo.find()
}
}
@@ -118,15 +157,13 @@ export default PostService
In the constructor, you can use dependency injection to get access to instances of services and repositories. Here, you initialize class fields `postRepository` and `manager`. The `manager` is a [Typeorm Entity Manager](https://typeorm.io/working-with-entity-manager).
Then, in the method `list`, you can obtain an instance of the `PostRepository` using `this.manager_.getCustomRepository` passing it `this.postRepository` as a parameter. This lets you use [Custom Repositories with Typeorm](https://typeorm.io/custom-repository) to create custom methods in your repository that work with the data in your database.
Then, in the method `list`, you can create an instance of the `PostRepository` using the `this.manager_.withRepository` method passing it `this.postRepository` as a parameter.
After getting an instance of the repository, you can then use [Typeorm’s Repository methods](https://typeorm.io/repository-api) to perform Create, Read, Update, and Delete (CRUD) operations on your entity.
If you need access to your entity in endpoints, you can then use the methods you define in the service.
After getting an instance of the repository, you can then use [Typeorm’s Repository methods](https://typeorm.io/repository-api) to perform Create, Read, Update, and Delete (CRUD) operations on your entity. You can also use any custom methods that you defined in the Repository.
:::note
This same usage of repositories can be done in subscribers as well.
This same usage of repositories can be done in other resources such as subscribers or endpoints.
:::
@@ -142,5 +179,5 @@ await postRepository.softDelete(post.id)
## See Also
- [Migrations Overview](./migrations/overview.mdx)
- [Extend Entity](./extend-entity.md)
- [Create a Plugin](../plugins/create.md)
@@ -0,0 +1,158 @@
---
description: 'Learn how to extend a core entity in Medusa to add custom attributes.'
addHowToData: true
---
# How to Extend an Entity
In this document, you’ll learn how to extend a core entity in Medusa.
## Overview
Medusa uses entities to represent tables in the database. As you build your custom commerce application, you’ll often need to add your own properties to those entities. This guide explains the necessary steps to extend core Medusa entities.
This guide will use the Product entity as an example to demonstrate the steps.
### Word of Caution about Overriding
Extending entities to add new attributes or methods shouldn't cause any issues within your commerce application. However, if you extend them to override their existing methods or attributes, you should be aware that this could have negative implications, such as unanticipated bugs, especially when you try to upgrade the core Medusa package to a newer version.
---
## Step 1: Create Entity File
In your Medusa backend, create the file `src/models/product.ts`. This file will hold your extended entity.
Note that the name of the file must be the same as the name of the original entity in the core package. Since in this guide you’re overriding the Product entity, it’s named `product` to match the core. If you’re extending the customer entity, for example, the file should be named `customer.ts`.
---
## Step 2: Implement Extended Entity
In the file you created, you can import the entity you’re extending from the core package, then create a class that extends that entity. You can add in that class the new attributes and methods.
Here’s an example of extending the Product entity:
```ts title=src/models/product.ts
import { Column, Entity } from "typeorm"
import {
// alias the core entity to not cause a naming conflict
Product as MedusaProduct,
} from "@medusajs/medusa"
@Entity()
export class Product extends MedusaProduct {
@Column()
customAttribute: string
}
```
---
## (Optional) Step 3: Create a TypeScript Declaration File
If you’re using JavaScript instead of TypeScript in your implementation, you can skip this step.
To ensure that TypeScript is aware of your extended entity and affects the typing of the Medusa package itself, create the file `src/index.d.ts` with the following content:
```ts title=src/index.d.ts
export declare module "@medusajs/medusa/dist/models/product" {
declare interface Product {
customAttribute: string;
}
}
```
Notice that you must pass the attributes you added to the entity into the `interface`. The attributes will be merged with the attributes defined in the core `Product` entity.
---
## Step 4: Extend Repository
As the entity is used throughout the commerce application through its repository, the core package will not actually be aware that the entity was extended unless you also extend the repository.
The steps here are similar to those described in the [How to Extend a Repository documentation](./extend-repository.md), however, the implementation is a little different.
Start by creating the repository file `src/repositories/product.ts`. As mentioned in the repository documentation, the name of the file should be the same as the name in the core. So, if you’re extending another repository, use the file name of that repository instead.
Then, in the file, add the following content:
```ts title=src/repositories/product.ts
import { Product } from "../models/product"
import {
dataSource,
} from "@medusajs/medusa/dist/loaders/database"
import {
// alias the core repository to not cause a naming conflict
ProductRepository as MedusaProductRepository,
} from "@medusajs/medusa/dist/repositories/product"
export const ProductRepository = dataSource
.getRepository(Product)
.extend({
// it is important to spread the existing repository here.
// Otherwise you will end up losing core properties.
// you also update the target to the extended entity
...Object.assign(
MedusaProductRepository,
{ target: Product }
),
// you can add other customizations as well...
})
export default ProductRepository
```
Instead of just spreading the properties of the `MedusaProductRepository` as you did when extending a repository, you have to change the value of the `target` property to be the entity you created.
---
## Step 5: Create Migration
To reflect your entity changes on the database schema, you must create a migration with those changes.
You can learn how to create or generate a migration in [this documentation](./migrations/create.md).
Here’s an example of a migration of the entity extended in this guide:
```ts title=src/migration/1680013376180-changeProduct.ts
import { MigrationInterface, QueryRunner } from "typeorm"
class changeProduct1680013376180 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE \"product\"" +
" ADD COLUMN \"customAttribute\" text"
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE \"product\" DROP COLUMN \"customAttribute\""
)
}
}
export default changeProduct1680013376180
```
---
## Step 6: Use Custom Entity
For changes to take effect, you must transpile your code by running the `build` command in the root of the Medusa backend:
```bash npm2yarn
npm run build
```
Then, run the following command to migrate your changes to the database:
```bash npm2yarn
medusa migrations run
```
You should see that your migration was executed, which means your changes were reflected in the database schema.
You can now use your extended entity and its repository throughout your commerce application.
@@ -0,0 +1,131 @@
---
description: 'Learn how to extend a core repository in Medusa to add custom methods.'
addHowToData: true
---
# How to Extend a Repository
In this document, you’ll learn how to extend a core repository in Medusa.
## Overview
Medusa uses Typeorm’s Repositories to perform operations on an entity, such as retrieve or update the entity. Typeorm already provides these basic functionalities within a repository, but sometimes you need to implement a custom implementation to handle the logic behind these operations differently. You might also want to add custom methods related to processing entities that aren’t available in the default repositories.
In this guide, you’ll learn how to extend a repository in the core Medusa package. This guide will use the Product repository as an example to demonstrate the steps.
### Word of Caution about Overriding
Extending repositories to add new methods shouldn't cause any issues within your commerce application. However, if you extend them to override their existing methods, you should be aware that this could have negative implications, such as unanticipated bugs, especially when you try to upgrade the core Medusa package to a newer version.
---
## Step 1: Create Repository File
In your Medusa backend, create the file `src/repositories/product.ts`. This file will hold your extended repository.
Note that the name of the file must be the same as the name of the original repository in the core package. Since in this guide you’re extending the Product repository, it’s named `product` to match the core. If you’re extending the customer repository, for example, the file should be named `customer.ts`.
---
## Step 2: Implement Extended Repository
In the file you created, you must retrieve both the repository you're extending along with its entity from the core. You’ll then use the data source exported from the core package to extend the repository.
:::tip
A data source is Typeorm’s connection settings that allows you to connect to your database. You can learn more about it in [Typeorm’s documentation](https://typeorm.io/data-source).
:::
Here’s an example of the implementation of the extended Product repository:
```ts title=src/repositories/product.ts
import { Product } from "@medusajs/medusa"
import {
dataSource,
} from "@medusajs/medusa/dist/loaders/database"
import {
// alias the core repository to not cause a naming conflict
ProductRepository as MedusaProductRepository,
} from "@medusajs/medusa/dist/repositories/product"
export const ProductRepository = dataSource
.getRepository(Product)
.extend({
// it is important to spread the existing repository here.
// Otherwise you will end up losing core properties
...MedusaProductRepository,
/**
* Here you can create your custom function
* For example
*/
customFunction(): void {
// TODO add custom implementation
return
},
})
export default ProductRepository
```
You first import all necessary resources from the core package: the `Product` entity, the `dataSource` instance, and the core’s `ProductRepository` aliased as `MedusaProductRepository` to avoid naming conflict.
You then use the `dataSource` instance to retrieve the `Product` entity’s repository and extend it using the repository’s `extend` method. This method is available as part of Typeorm Repository API. This method returns your extended repository.
The `extend` method accepts an object with all the methods to add to the extended repository.
You must first add the properties of the repository you’re extending, which in this case is the product repository (aliased as `MedusaProductRepository`). This will ensure you don’t lose core methods, which can lead to the core not working as expected. You add use the spread operator (`…`) with the `MedusaProductRepository` to spread its properties.
After that, you can add your custom methods to the repository. In the example above, you add the method `customFunction`. You can use any name for your methods.
---
## Step 3: Use Your Extended Repository
You can now use your extended repository in other resources such as services or endpoints.
Here’s an example of using it in an endpoint:
```ts
import ProductRepository from "./path/to/product.ts"
import EntityManager from "@medusajs/medusa"
export default () => {
// ...
router.get("/custom-endpoint", (req, res) => {
// ...
const productRepository: typeof ProductRepository =
req.scope.resolve(
"productRepository"
)
const manager: EntityManager = req.scope.resolve("manager")
const productRepo = manager.withRepository(
productRepository
)
productRepo.customFunction()
// ...
})
}
```
---
## Step 4: Test Your Implementation
For changes to take effect, you must transpile your code by running the `build` command in the root of the Medusa backend:
```bash npm2yarn
npm run build
```
Then, run the following command to start your backend:
```bash npm2yarn
npm run start
```
You should see your custom implementation working as expected.
@@ -22,36 +22,29 @@ The migration file must be inside the `src/migrations` directory. When you run t
<details>
<summary>Generating Migrations for Entities</summary>
You can alternatively use Typeorm's `generate` command to generate a Migration file from existing entity classes. As Medusa uses v0.2.45 of Typeorm, you have to create a `ormconfig.json` first before using the `generate` command.
You can alternatively use Typeorm's `generate` command to generate a Migration file from existing entity classes. As of v1.8, Medusa uses Typeorm v0.3.x. You have to create a [DataSource](https://typeorm.io/data-source) first before using the `migration:generate` command.
:::note
For example, create the file `datasource.js` in the root of your Medusa server with the following content:
Typeorm will be updated to the latest version in v1.8.0 of Medusa.
:::
For example, create the file `ormconfig.json` in the root of your Medusa server with the following content:
```json
{
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "<YOUR_DB_USERNAME>",
"password": "<YOUR_DB_PASSWORD>",
"database": "<YOUR_DB_NAME>",
"synchronize": true,
"logging": false,
"entities": [
"dist/models/**/*.js"
```js
const { DataSource } = require("typeorm")
const AppDataSource = new DataSource({
type: "postgres",
port: 5432,
username: "<YOUR_DB_USERNAME>",
password: "<YOUR_DB_PASSWORD>",
database: "<YOUR_DB_NAME>",
entities: [
"dist/models/*.js",
],
"migrations": [
"dist/migrations/**/*.js"
migrations: [
"dist/migrations/*.js",
],
"cli": {
"entitiesDir": "src/models",
"migrationsDir": "src/migrations"
}
})
module.exports = {
datasource: AppDataSource,
}
```
@@ -66,7 +59,7 @@ Typeorm will be updated to the latest version in v1.8.0 of Medusa.
Finally, run the following command to generate a Migration for your new entity:
```bash
npx typeorm@0.2.45 migration:generate -n PostCreate
npx typeorm migration:generate -d datasource.js src/migrations/PostCreate
```
Where `PostCreate` is just an example of the name of the migration to generate. The migration will then be generated in `src/migrations/<TIMESTAMP>-PostCreate.ts`. You can then skip to step 3 of this guide.
+20 -2
View File
@@ -76,10 +76,10 @@ Developers can create custom entities in the Medusa backend, a plugin, or in a C
{
type: 'link',
href: '/development/entities/create',
label: 'Create an Endpoint',
label: 'Create an Entity',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to create endpoints in Medusa.'
description: 'Learn how to create an entity in Medusa.'
}
},
{
@@ -91,4 +91,22 @@ Developers can create custom entities in the Medusa backend, a plugin, or in a C
description: 'Learn how to create migrations in Medusa.'
}
},
{
type: 'link',
href: '/development/entities/extend-entity',
label: 'Extend an Entity',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to extend a core Medusa entity.'
}
},
{
type: 'link',
href: '/development/entities/extend-repository',
label: 'Extend a Repository',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to extend a core Medusa repository.'
}
},
]} />
@@ -0,0 +1,260 @@
---
description: 'In this document, you’ll learn how to create an events module, then test and publish the module as an NPM package.'
addHowToData: true
---
# Create Events Module
In this document, you’ll learn how to create an events module.
## Overview
Medusa provides ready-made modules for events, including the local and Redis modules. If you prefer another technology used for managing events, you can build a module locally and use it in your Medusa backend. You can also publish to NPM and reuse it across multiple Medusa backend instances.
In this document, you’ll learn how to build your own Medusa events module, mainly focusing on creating the event bus service and the available methods you need to implement within your module.
---
## (Optional) Step 0: Prepare Module Directory
Before you start implementing your module, it's recommended to prepare the directory or project holding your custom implementation.
You can refer to the [Project Preparation step in the Create Module documentation](../modules/create.mdx#optional-step-0-project-preparation) to learn how to do that.
---
## Step 1: Create the Service
Create the file `services/event-bus-custom.ts` which will hold your event bus service. Note that the name of the file is recommended to be in the format `event-bus-<service_name>` where `<service_name>` is the name of the service you’re integrating. For example, `event-bus-redis`.
Add the following content to the file:
```ts title=services/event-bus-custom.ts
import { EmitData, EventBusTypes } from "@medusajs/types"
import { AbstractEventBusModuleService } from "@medusajs/utils"
class CustomEventBus extends AbstractEventBusModuleService {
async emit<T>(
eventName: string,
data: T,
options: Record<string, unknown>
): Promise<void>;
async emit<T>(data: EmitData<T>[]): Promise<void>;
async emit(
eventName: unknown,
data?: unknown,
options?: unknown
): Promise<void> {
throw new Error("Method not implemented.")
}
}
export default CustomEventBus
```
This creates the class `CustomEventBus` that implements the `AbstractEventBusModuleService` class imported from `@medusajs/utils`. Feel free to rename the class to what’s relevant for your event bus service.
In the class you must implement the `emit` method. You can optionally implement the `subscribe` and `unsubscribe` methods.
---
## Step 2: Implement Methods
### Note About the eventToSubscribersMap Property
The `AbstractEventBusModuleService` implements two methods for handling subscription: `subscribe` and `unsubscribe`. In these methods, the subscribed handler methods are managed within a class property `eventToSubscribersMap`, which is a JavaScript Map. They map keys are the event names, whereas the value of each key is an array of subscribed handler methods.
In your custom implementation, you can use this property to manage the subscribed handler methods. For example, you can get the subscribers of a method using the `get` method of the map:
```ts
const eventSubscribers =
this.eventToSubscribersMap.get(eventName) || []
```
Alternatively, you can implement custom logic for the `subscribe` and `unsubscribe` events, which is explained later in this guide.
### constructor
The `constructor` method of a service allows you to prepare any third-party client or service necessary to be used in other methods. It also allows you to get access to the module’s options which are typically defined in `medusa-config.js`, and to other services and resources in the Medusa backend using [dependency injection](../fundamentals/dependency-injection.md).
Here’s an example of how you can use the `constructor` to store the options of your module:
<!-- eslint-disable prefer-rest-params -->
```ts title=services/event-bus-custom.ts
class CustomEventBus extends AbstractEventBusModuleService {
protected readonly moduleOptions: Record<string, any>
constructor({
// inject resources from the Medusa backend
// for example, you can inject the logger
logger,
}, options) {
super(...arguments)
this.moduleOptions = options
}
// ...
}
```
### emit
The `emit` method is used to push an event from Medusa into your messaging system. Typically, the subscribers to that event would then pick up the message and execute their asynchronous tasks.
The `emit` method has two different signatures:
1. The first signature accepts three parameters. The first parameter is `eventName` being a required string indicating the name of the event to trigger. The second parameter is `data` being the optional data to send to subscribers of that event. The third optional parameter `options` which can be used to pass options specific to the event bus.
2. The second signature accepts one parameter, which is an array of objects having three properties: `eventName`, `data`, and `options`. These are the same as the parameters that can be passed in the first signature. This signature allows emitting more than one event.
The `options` parameter depends on the event bus integrating. For example, the Redis event bus accept the following options:
```ts title=services/event-bus-custom.ts
type JobData<T> = {
eventName: string
data: T
completedSubscriberIds?: string[] | undefined
}
```
You can implement your method in a way that supports both signatures by checking the type of the first input. For example:
```ts title=services/event-bus-custom.ts
class CustomEventBus extends AbstractEventBusModuleService {
// ...
async emit<T>(
eventName: string,
data: T,
options: Record<string, unknown>
): Promise<void>;
async emit<T>(data: EmitData<T>[]): Promise<void>;
async emit<T>(
eventOrData: string | EmitData<T>[],
data?: T,
options: Record<string, unknown> = {}
): Promise<void> {
const isBulkEmit = Array.isArray(eventOrData)
// emit event
}
}
```
### (optional) subscribe
As mentioned earlier, this method is already implemented in the `AbstractEventBusModuleService` class. This section explains how you can implement your custom subscribe logic if necessary.
The `subscribe` method attaches a handler method to the specified event, which is run when the event is triggered. It is typically used inside a subscriber class.
The `subscribe` method accepts three parameters:
1. The first parameter `eventName` is a required string. It indicates which event the handler method is subscribing to.
2. The second parameter `subscriber` is a required function that performs an action when the event is triggered.
3. The third parameter `context` is an optional object that has the property `subscriberId`. Subscriber IDs are useful to differentiate between handler methods when retrying a failed method. It’s also useful for unsubscribing an event handler. Note that if you must implement the mechanism around assigning IDs to subscribers when you override the `subscribe` method.
The implementation of this method depends on the service you’re using for the event bus:
```ts title=services/event-bus-custom.ts
class CustomEventBus extends AbstractEventBusModuleService {
// ...
subscribe(
eventName: string | symbol,
subscriber: EventBusTypes.Subscriber,
context?: EventBusTypes.SubscriberContext): this {
// TODO implement subscription
}
}
```
### (optional) unsubscribe
As mentioned earlier, this method is already implemented in the `AbstractEventBusModuleService` class. This section explains how you can implement your custom unsubscribe logic if necessary.
The `unsubscribe` method is used to unsubscribe a handler method from an event.
The `unsubscribe` method accepts three parameters:
1. The first parameter `eventName` is a required string. It indicates which event the handler method is unsubscribing from.
2. The second parameter `subscriber` is a required function that was initially subscribed to the event.
3. The third parameter `context` is an optional object that has the property `subscriberId`. It can be used to specify the ID of the subscriber to unsubscribe.
The implementation of this method depends on the service you’re using for the event bus:
```ts title=services/event-bus-custom.ts
class CustomEventBus extends AbstractEventBusModuleService {
// ...
unsubscribe(
eventName: string | symbol,
subscriber: EventBusTypes.Subscriber,
context?: EventBusTypes.SubscriberContext): this {
// TODO implement subscription
}
}
```
---
## Step 3: Export the Service
After implementing the event bus service, you must export it so that the Medusa backend can use it.
Create the file `index.ts` with the following content:
```ts title=services/event-bus-custom.ts
import { ModuleExports } from "@medusajs/modules-sdk"
import { CustomEventBus } from "./services"
const service = CustomEventBus
const moduleDefinition: ModuleExports = {
service,
}
export default moduleDefinition
```
This exports a module definition, which requires at least a `service`. If you named your service something other than `CustomEventBus`, make sure to replace it with that.
You can learn more about what other properties you can export in your module definition in the [Create a Module documentation](../modules/create.mdx#step-2-export-module).
---
## Step 4: Test your Module
You can test your module in the Medusa backend by referencing it in the configurations.
To do that, add the module to the exported configuration in `medusa-config.js` as follows:
```js title=medusa-config.js
module.exports = {
// ...
modules: {
// ...
cacheService: {
resolve: "path/to/custom-module",
options: {
// any necessary options
},
},
},
}
```
Make sure to replace the `path/to/custom-module` with a relative path from your Medusa backend to your module. You can learn more about module reference in the [Create Module documentation](../modules/create.mdx#module-reference).
You can also add any necessary options to the module.
Then, to test the module, run the Medusa backend which also runs your module:
```bash npm2yarn
npm run start
```
---
## (Optional) Step 5: Publish your Module
You can publish your events module to NPM. This can be useful if you want to reuse your module across Medusa backend instances, or want to allow other developers to use it.
You can refer to the [Publish Module documentation](../modules/publish.md) to learn how to publish your module.
@@ -7,14 +7,6 @@ addHowToData: true
In this document, you’ll learn how to create a [Subscriber](./subscribers.mdx) in Medusa that listens to events to perform an action.
## Prerequisites
Medusa's event system works by pushing data to a Queue that each handler then gets notified of. The queuing system is based on Redis, so it's required for subscribers to work.
You can learn how to [install Redis](../backend/prepare-environment.mdx#redis) and [configure it with Medusa](../backend/configurations.md#redis) before you get started.
---
## Implementation
A subscriber is a TypeScript or JavaScript file that is created under `src/subscribers`. Its file name, by convension, should be the class name of the subscriber without the word `Subscriber`. For example, if the subscriber is `HelloSubscriber`, the file name should be `hello.ts`.
@@ -41,12 +33,25 @@ export default OrderNotifierSubscriber
This subscriber registers the method `handleOrder` as one of the handlers of the `order.placed` event. The method `handleOrder` will be executed every time an order is placed. It receives the order ID in the `data` parameter. You can then use the order’s details to perform any kind of task you need.
:::note
:::tip
The `data` object won't contain other order data. Only the ID of the order. You can retrieve the order information using the `orderService`.
For the `order.placed` event, the `data` object won't contain other order data. Only the ID of the order. You can retrieve the order information using the `orderService`.
:::
### Subscriber ID
The `subscribe` method of the `eventBusService` accepts a third optional parameter which is a context object. This object has a property `subscriberId` with its value being a string. This ID is useful when there is more than one handler method attached to a single event or if you have multiple Medusa backends running. This allows the events bus service to differentiate between handler methods when retrying a failed one.
If a subscriber ID is not passed on subscription, all handler methods are run again. This can lead to data inconsistencies or general unwanted behavior in your system. On the other hand, if you want all handler methods to run again when one of them fails, you can omit passing a subscriber ID.
An example of using the subscribe method with the third parameter:
```ts
eventBusService.subscribe("order.placed", this.handleOrder, {
subscriberId: "my-unique-subscriber",
})
```
---
## Using Services in Subscribers
@@ -55,7 +60,7 @@ You can access any service through the dependencies injected to your subscriber
For example:
```ts
```ts title=src/subscribers/orderNotifier.ts
class OrderNotifierSubscriber {
constructor({ productService, eventBusService }) {
this.productService = productService
@@ -71,7 +76,7 @@ class OrderNotifierSubscriber {
You can then use `this.productService` anywhere in your subscriber’s methods. For example:
```ts
```ts title=src/subscribers/orderNotifier.ts
class OrderNotifierSubscriber {
// ...
handleOrder = async (data) => {
+383 -2
View File
@@ -826,6 +826,193 @@ Object of the following format:
---
## Inventory Item Events
This section holds all events related to inventory items.
<table class="reference-table">
<thead>
<tr>
<th>
Event Name
</th>
<th>
Description
</th>
<th>
Event Data Payload
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
`inventory-item.created`
</td>
<td>
Triggered when an inventory item is created.
</td>
<td>
Object of the following format:
```js noReport noCopy
{
id // string ID of the inventory item
}
```
</td>
</tr>
<tr>
<td>
`inventory-item.updated`
</td>
<td>
Triggered when an inventory item is updated.
</td>
<td>
Object of the following format:
```js noReport noCopy
{
id // string ID of the inventory item
}
```
</td>
</tr>
<tr>
<td>
`inventory-item.deleted`
</td>
<td>
Triggered when an inventory item is deleted.
</td>
<td>
Object of the following format:
```js noReport noCopy
{
id // string ID of the inventory item
}
```
</td>
</tr>
</tbody>
</table>
---
## Inventory Level Events
This section holds all events related to inventory levels.
<table class="reference-table">
<thead>
<tr>
<th>
Event Name
</th>
<th>
Description
</th>
<th>
Event Data Payload
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
`inventory-level.created`
</td>
<td>
Triggered when an inventory level is created.
</td>
<td>
Object of the following format:
```js noReport noCopy
{
id // string ID of the inventory level
}
```
</td>
</tr>
<tr>
<td>
`inventory-level.updated`
</td>
<td>
Triggered when an inventory level is updated.
</td>
<td>
Object of the following format:
```js noReport noCopy
{
id // string ID of the inventory level
}
```
</td>
</tr>
<tr>
<td>
`inventory-level.deleted`
</td>
<td>
Triggered when an inventory level is deleted, which can be done either directly using its ID or based on the ID of a location. The returned ID depends on how the inventory level was deleted.
</td>
<td>
Object of the following format:
```js noReport noCopy
{
id // (optional) string ID of the inventory level, available if it was deleted directly
location_id // (optional) string ID of location, available if level was deleted by location ID
}
```
</td>
</tr>
</tbody>
</table>
---
## Invite Events
This section holds all events related to invites.
@@ -1817,7 +2004,7 @@ Triggered when the capturing of a payment fails.
The entire payment passed as an object. You can refer to the [Payment entity](../../references/entities/classes/Payment.md) for an idea of what fields to expect.
In addition, an error object is passed within the same object as the Payment provider:
In addition, an error object is passed within the same object as the Payment Processor:
```js noReport noCopy
{
@@ -2076,6 +2263,12 @@ Object of the following format:
This section holds all events related to product categories.
:::note
Product Category feature is currently in beta mode and guarded by a feature flag. You can learn how to enable it in the [Product Categories documentation](../../modules/products/categories.md).
:::
<table class="reference-table">
<thead>
<tr>
@@ -2438,6 +2631,101 @@ Object of the following format:
---
## Reservation Item Events
This section holds all events related to reservation items.
<table class="reference-table">
<thead>
<tr>
<th>
Event Name
</th>
<th>
Description
</th>
<th>
Event Data Payload
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
`reservation-item.created`
</td>
<td>
Triggered when a reservation item is created.
</td>
<td>
Object of the following format:
```js noReport noCopy
{
id // string ID of the reservation item
}
```
</td>
</tr>
<tr>
<td>
`reservation-item.updated`
</td>
<td>
Triggered when an reservation item is updated.
</td>
<td>
Object of the following format:
```js noReport noCopy
{
id // string ID of the reservation item
}
```
</td>
</tr>
<tr>
<td>
`reservation-item.deleted`
</td>
<td>
Triggered when a reservation item is deleted, which can be done either directly using its ID or based on the ID of a location or a line item. The returned ID depends on how the reservation item was deleted.
</td>
<td>
Object of the following format:
```js noReport noCopy
{
id // (optional) string ID of the reservation item, available if it was deleted directly
location_id // (optional) string ID of location, available if item was deleted by location ID
line_item_id // (optional) string ID of line item, available if reservation item was deleted by line item ID
}
```
</td>
</tr>
</tbody>
</table>
---
## Sales Channel Events
This section holds all events related to sales channels.
@@ -2533,6 +2821,99 @@ Object of the following format:
---
## Stock Location Events
This section holds all events related to stock locations.
<table class="reference-table">
<thead>
<tr>
<th>
Event Name
</th>
<th>
Description
</th>
<th>
Event Data Payload
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
`stock-location.created`
</td>
<td>
Triggered when a stock location is created.
</td>
<td>
Object of the following format:
```js noReport noCopy
{
id // string ID of the stock location
}
```
</td>
</tr>
<tr>
<td>
`stock-location.updated`
</td>
<td>
Triggered when an stock location is updated.
</td>
<td>
Object of the following format:
```js noReport noCopy
{
id // string ID of the stock location
}
```
</td>
</tr>
<tr>
<td>
`stock-location.deleted`
</td>
<td>
Triggered when a stock location is deleted.
</td>
<td>
Object of the following format:
```js noReport noCopy
{
id // string ID of the stock location
}
```
</td>
</tr>
</tbody>
</table>
---
## Swap Events
This section holds all events related to swaps.
@@ -2958,6 +3339,6 @@ Object of the following format:
## See Also
- [Events architecture overview](./index.md)
- [Events overview](./index.mdx)
- [Use services in subscribers](./create-subscriber.md#using-services-in-subscribers)
- [Create a notification provider](../notification/overview.mdx)
-227
View File
@@ -1,227 +0,0 @@
---
description: 'Learn how the events system is implemented in Medusa. It is built on a publish-subscribe architecture. The Medusa core publishes events when certain actions take place.'
---
# Events Architecture
In this document, you'll learn how the events system is implemented in Medusa.
## Overview
The events system in Medusa is built on a publish/subscribe architecture. The Medusa core publishes events when certain actions take place.
Those events can be subscribed to using subscribers. When you subscribe to an event, you can perform a task asynchronusly every time the event is triggered.
:::info
You can learn more about subscribers and their use cases in the [Subscribers](./subscribers.mdx) documentation.
:::
---
## Publishing and Subscribing
The `EventBusService` is responsible for publishing and processing events.
:::note
The current implementation of the `EventBusService` is powered by Redis. However, an upcoming version of Medusa introduces an event bus module. This will allow you to use any publishing and subscribing provider. That will not change the general purpose and flow of the `EventBusService`.
:::
The `EventBusService` exposes two methods in its public API for event processing; `emit` and `subscribe`.
### emit
The `emit` method accepts as a first parameter the event name. It adds it to a Bull queue (powered by Redis) as a job, and processes it asynchronously.
The second parameter contains any data that should be emitted with the event. Subscribers that handle the event will receive that data as a method parameter.
The third parameter is an options object. It accepts options related to the number of retries if a subscriber handling the event fails, the delay time, and more. The options are explained in a [later section](#retrying-handlers)
The `emit` method has the following signature:
```ts
export default class EventBusService {
// ...
async emit<T>(
eventName: string,
data: T,
options: Record<string, unknown> &
EmitOptions = { attempts: 1 }
): Promise<StagedJob | void>
}
```
Here's an example of how you can emit an event using the `EventBusService`:
```ts
eventBusService.emit(
"product.created",
{ id: "prod_..." },
{ attempts: 2 }
)
```
The `EventBusService` emits the event `product.created` by passing the event name as a first argument. An object is passed as a second argument which is the data passed to the event handler methods in subscribers. This object contains the ID of the product.
Options are passed in the third argument. The `attempt` property specifies how many times the subscriber should be retried if it fails (by default it's one).
### subscribe
The `subscribe` method will attach a handler method to the specified event, which is run when the event is triggered. It is usually used insde a subscriber class.
The `subscribe` method accepts the event name as the first parameter. This is the event that the handler method will attach to.
The second parameter is the handler method that will be triggered when the event is emitted.
The third parameter is an optional `context` parameter. It allows you to configure the ID of the handler method.
The `subscribe` method has the following signature:
```ts
export default class EventBusService {
// ...
subscribe(
event: string | symbol,
subscriber: Subscriber,
context?: SubscriberContext
): this
}
```
Here's an example of how you can subscribe to an event using the `EventBusService`:
```ts title=src/subscribers/my.ts
import { EventBusService } from "@medusajs/medusa"
class MySubscriber {
constructor({
eventBusService: EventBusService,
}) {
eventBusService.subscribe("product.created", (data) => {
// TODO handle event
console.log(data.id)
})
}
}
```
In the constructor of a subscriber, you use the `EventBusService` to subscribe to the event `product.created`. In the handler method, you can perform a task every time the product is created. Notice how the handler method accepts the `data` as a parameter as explain in the previous section.
:::note
You can learn more about how to create a subscriber in [this documentation](./create-subscriber.md)
:::
---
## Processing Events
In the `EventBusService` service, the `worker_` method defines the logic run for each event emitted into the queue.
By default, all handler methods to that event are retrieved and, for each of the them, the stored data provided as a second parameter in `emit` is passed as an argument.
---
## Retrying Handlers
A handler method might fail to process an event. This could happen because it communicates with a third party service currently down or due to an error in its logic.
In some cases, you might want to retry those failed handlers.
As briefly explained earlier, you can pass options when emitting an event as a third argument that are used to configure how the queue worker processes your job. If you pass `attempts` upon emitting the event, the processing of a handler method is retried when it fails.
Aside from `attempts`, there are other options to futher configure the retry mechanism:
```ts
type EmitOptions = {
delay?: number
attempts: number
backoff?: {
type: "fixed" | "exponential"
delay: number
}
}
```
Here's what each of these options mean:
- `delay`: delay the triggering of the handler methods by a number of milliseconds.
- `attempts`: the number of times a subscriber handler should be retried when it fails.
- `backoff`: the wait time between each retry
### Note on Subscriber IDs
If you have more than one handler methods attached to a single event, or if you have multiple backend instances running, you must pass a subscriber ID as a third parameter to the `subscribe` method. This allows the `EventBusService` to differentiate between handler methods when retrying a failed one.
If a subscriber ID is not passed on subscription, all handler methods are run again. This can lead to data inconsistencies or general unwanted behavior in your system.
On the other hand, if you want all handler methods to run again when one of them fails, you can omit passing a subscriber ID.
An example of passing a subscriber ID:
```ts title=src/subscribers/my.ts
import { EventBusService } from "@medusajs/medusa"
class MySubscriber {
constructor({
eventBusService: EventBusService,
}) {
eventBusService.subscribe(
"product.created",
(data) => {
// TODO handle event
console.log(data.id)
},
"my-unique-subscriber")
}
}
```
:::info
You can learn more about subscriber IDs in [Bull's documentation](https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md#queueadd).
:::
---
## Database transactions
<!-- vale docs.Acronyms = NO -->
Transactions in Medusa ensure atomicity, consistency, isolation, and durability, or ACID, guarantees for operations in the Medusa core.
<!-- vale docs.Acronyms = YES -->
In many cases, [services](../services/overview.mdx) typically update resources in the database and emit an event within a transactional operation. To ensure that these events don't cause data inconsistencies (for example, a plugin subscribes to an event to contact a third-party service, but the transaction fails) the concept of a staged job is introduced.
Instead of events being processed immediately, they're stored in the database as a staged job until they're ready. In other words, until the transaction has succeeded.
This rather complex logic is abstracted away from the consumers of the `EventBusService`, but here's an example of the flow when an API request is made:
1. API request starts.
2. Transaction is initiated.
3. Service layer performs some logic.
4. Events are emitted and stored in the database for eventual processing.
5. Transaction is committed.
6. API request ends.
7. Events in the database become visible.
To pull staged jobs from the database, a separate enqueuer polls the database every three seconds to discover new visible jobs. These jobs are then added to the queue and processed as described in the [Processing](#processing-events) section earlier.
:::info
This pattern is heavily inspired by the [Transactionally-staged Job Drain described in this blog post](https://brandur.org/job-drain).
:::
---
## See Also
- [Events reference](./events-list.md)
- [Create a subscriber](./create-subscriber.md)
+131
View File
@@ -0,0 +1,131 @@
---
description: 'Learn how the events system is implemented in Medusa. It is built on a publish-subscribe architecture. The Medusa core publishes events when certain actions take place.'
---
import DocCardList from '@theme/DocCardList';
import Icons from '@theme/Icon';
# Events
In this document, you’ll learn what events are and why they’re useful in Medusa.
## Overview
Events are used in Medusa to inform different parts of the commerce ecosystem that this event occurred. For example, when an order is placed, the `order.placed` event is triggered, which informs notification services like SendGrid to send a confirmation email to the customer.
The events system in Medusa is built on a publish/subscribe architecture. The Medusa core publish an event when an action takes place, and modules, plugins, or other forms of customizations can subscribe to that event. [Subscribers](./subscribers.mdx) can then perform a task asynchronously when the event is triggered.
Although the core implements the main logic behind the events system, you’ll need to use an event module that takes care of the publish/subscribe functionality such as subscribing and emitting events. Medusa provides modules that you can use both for development and production, including Redis and Local modules.
---
## Database Transactions
Transactions in Medusa ensure Atomicity, Consistency, Isolation, and Durability (ACID) guarantees for operations in the Medusa core.
In many cases, services typically update resources in the database and emit an event within a transactional operation. To ensure that these events don't cause data inconsistencies (for example, a plugin subscribes to an event to contact a third-party service, but the transaction fails) the concept of a staged job is introduced.
Instead of events being processed immediately, they're stored in the database as a staged job until they're ready. In other words, until the transaction has succeeded.
This rather complex logic is abstracted away from the consumers of the EventBusService, but here's an example of the flow when an API request is made:
- API request starts.
- Transaction is initiated.
- Service layer performs some logic.
- Events are emitted and stored in the database for eventual processing.
- Transaction is committed.
- API request ends.
- Events in the database become visible.
To pull staged jobs from the database, a separate enqueuer polls the database every three seconds to discover new visible jobs. These jobs are then added to the queue and processed as described in the Processing section earlier.
:::note
This pattern is heavily inspired by the Transactionally-staged Job Drain described in this blog post.
:::
---
## Emitting Events
You can emit events in Medusa using the `EventBusService`. For example:
```ts
this.eventBusService.emit("custom-event", {
// attach any data to the event
})
```
You can also emit more than one event:
```ts
this.eventBusService.emit([
{
eventName: "custom-event-1",
data: {
// attach any data to the event
},
},
{
eventName: "custom-event-2",
data: {
// attach any data to the event
},
},
])
```
---
## Available Modules
Medusa’s default starter project comes with the local event module (`@medusajs/event-bus-local`). For production environments, it’s recommended to use the Redis event module package (`@medusajs/event-bus-redis`) that you can install.
<DocCardList colSize={6} items={[
{
type: 'link',
href: '/development/events/modules/redis',
label: 'Redis',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to install Redis events module in Medusa.'
}
},
{
type: 'link',
href: '/development/events/modules/local',
label: 'Local',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to install local events module in Medusa.'
}
},
]} />
---
## Custom Development
Developers can create custom event modules, allowing them to integrate any third-party services or logic to handle this functionality. Developers can also create and use subscribers to handle events in Medusa.
<DocCardList colSize={6} items={[
{
type: 'link',
href: '/development/events/create-module',
label: 'Create an Event Module',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to create an event module.'
}
},
{
type: 'link',
href: '/development/events/create-subscriber',
label: 'Create a Subscriber',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to create a subscriber.'
}
},
]} />
@@ -0,0 +1,68 @@
---
description: 'In this document, you’ll learn about the local events module and how you can install it in your Medusa backend.'
addHowToData: true
---
# Local Events Module
In this document, you’ll learn about the local events module and how you can install it in your Medusa backend.
## Overview
Medusa’s modular architecture allows developers to extend or completely replace the logic used for events. You can create a custom module, or you can use the modules Medusa provides.
One of these modules is the local events module. This module allows you to utilize Node EventEmitter for the events system in Medusa. The Node EventEmitter is limited to a single process environment. This module is useful for development and testing, but it’s recommended to use the [Redis events module](./redis.md) in production.
This document will you guide you through installing the local events module.
---
## Prerequisites
It’s assumed you already have a Medusa backend installed. If not, you can learn how to install it by following [this guide](../../backend/install.mdx).
---
## Step 1: Install the Module
In the root directory of your Medusa backend, install the Redis events module with the following command:
```bash npm2yarn
npm install @medusajs/event-bus-local
```
---
## Step 2: Add Configuration
In `medusa-config.js`, add the following to the exported object:
```js title=medusa-config.js
module.exports = {
// ...
modules: {
// ...
eventBus: {
resolve: "@medusajs/event-bus-local",
},
},
}
```
This registers the local events module as the main events service to use. This module does not require any options.
---
## Step 4: Test Module
To test the module, run the following command to start the Medusa backend:
```bash npm2yarn
npm run start
```
If the module was installed successfully, you should see the following message in the logs:
```bash noCopy noReport
Local Event Bus installed. This is not recommended for production.
```
@@ -0,0 +1,95 @@
---
description: 'In this document, you’ll learn about the Redis events module and how you can install it in your Medusa backend.'
addHowToData: true
---
# Redis Events Module
In this document, you’ll learn about the Redis events module and how you can install it in your Medusa backend.
## Overview
Medusa’s modular architecture allows developers to extend or completely replace the logic used for events. You can create a custom module, or you can use the modules Medusa provides.
One of these modules is the Redis module. This module allows you to utilize Redis for the event bus functionality. When installed, the Medusa’s events system is powered by BullMQ and `io-redis`. BullMQ is responsible for the message queue and worker, and `io-redis` is the underlying Redis client that BullMQ connects to for events storage.
This document will you guide you through installing the Redis module.
---
## Prerequisites
### Medusa Backend
It’s assumed you already have a Medusa backend installed. If not, you can learn how to install it by following [this guide](../../backend/install.mdx).
### Redis
You must have Redis installed and configured in your Medusa backend. You can learn how to install Redis in [their documentation](https://redis.io/docs/getting-started/installation/).
---
## Step 1: Install the Module
In the root directory of your Medusa backend, install the Redis events module with the following command:
```bash npm2yarn
npm install @medusajs/event-bus-redis
```
---
## Step 2: Add Environment Variable
The Redis events module requires a connection URL to Redis as part of its options. If you don’t already have an environment variable set for a Redis URL, make sure to add one:
```bash
EVENTS_REDIS_URL=<YOUR_REDIS_URL>
```
Where `<YOUR_REDIS_URL>` is a connection URL to your Redis instance.
---
## Step 3: Add Configuration
In `medusa-config.js`, add the following to the exported object:
```js title=medusa-config.js
module.exports = {
// ...
modules: {
// ...
eventBus: {
resolve: "@medusajs/event-bus-redis",
options: {
redisUrl: process.env.EVENTS_REDIS_URL,
},
},
},
}
```
This registers the Redis events module as the main event bus service to use. In the options, you pass `redisUrl` with the value being the environment variable you set. This is the only required option.
Other available options include:
- `queueName`: a string indicating the name of the BullMQ queue. By default, it’s `events-queue`.
- `queueOptions`: an object containing options for the BullMQ queue. You can learn about available options in [BullMQ’s documentation](https://api.docs.bullmq.io/interfaces/QueueOptions.html). By default, it’s an empty object.
- `redisOptions`: an object containing options for the Redis instance. You can learn about available options in [io-redis’s documentation](https://luin.github.io/ioredis/index.html#RedisOptions). By default, it’s an empty object.
---
## Step 4: Test Module
To test the module, run the following command to start the Medusa backend:
```bash npm2yarn
npm run start
```
If the module was installed successfully, you should see the following message in the logs:
```bash noCopy noReport
Connection to Redis in module 'event-bus-redis' established
```
@@ -9,16 +9,6 @@ import Icons from '@theme/Icon';
In this document, you'll learn what Subscribers are in Medusa.
## What are Events
In Medusa, there are events that are emitted when a certain action occurs. For example, if a customer places an order, the `order.placed` event is emitted with the order data.
The purpose of these events is to allow other parts of the platform, or third-party integrations, to listen to those events and perform a certain action. That is done by creating subscribers.
Medusa's queuing and events system is handled by Redis. So, you must have [Redis configured](../backend/prepare-environment.mdx#redis) on your backend to use subscribers.
---
## What are Subscribers
Subscribers register handlers for an events and allows you to perform an action when that event occurs. For example, if you want to send your customer an email when they place an order, then you can listen to the `order.placed` event and send the email when the event is emitted.
@@ -18,7 +18,7 @@ The retrieval, manipulation, and other utility methods related to that entity ar
The backend does not have any tightly-coupled frontend. Instead, it exposes [**Endpoints**](../endpoints/overview.mdx) which are REST APIs that frontends such as an admin or a storefront can use to communicate with the backend. Endpoints are [Express routes](https://expressjs.com/en/guide/routing.html).
Medusa also uses an [**Events Architecture**](../events/index.md) to trigger and handle events. Events are triggered when a specific action occurs, such as when an order is placed. To manage this events system, Medusa connects to a service that implements a pub/sub model, such as [Redis](https://redis.io/).
Medusa also uses an [**Events Architecture**](../events/index.mdx) to trigger and handle events. Events are triggered when a specific action occurs, such as when an order is placed. To manage this events system, Medusa connects to a service that implements a pub/sub model, such as [Redis](https://redis.io/).
Events can be handled using [**Subscribers**](../events/subscribers.mdx). Subscribers are TypeScript or JavaScript classes that add their methods as handlers for specific events. These handler methods are only executed when an event is triggered.
@@ -2,9 +2,9 @@
description: 'Learn what the dependency container is and how to use it in Medusa. Learn also what dependency injection is, and what the resources regsitered and their names are.'
---
# Dependency Injection
# Dependency Container and Injection
In this document, you’ll learn what the dependency injection is and how you can use it in Medusa.
In this document, you’ll learn what the dependency container is and how you can use it in Medusa with dependency injection.
## Introduction
@@ -36,13 +36,13 @@ The backend then registers all important resources in the container, which makes
The Medusa backend scans the core Medusa package, plugins, and your files in the `dist` directory and registers the following resources:
:::note
:::tip
Many resources are registered under their camel-case name. These resources are formatted by taking the name of the file, transforming it to camel case, then appending the folder name to the name. So, the `services/product.ts` service is registered as `productService`.
The Lifetime column indicates the lifetime of a service. Other resources that aren't services don't have a lifetime, which is indicated with the `-` in the column. You can learn about what a lifetime is in the [Create a Service](../services/create-service.md) documentation.
:::
<table class="reference-table">
<table class="reference-table table-col-4">
<thead>
<tr>
<th>
@@ -60,6 +60,11 @@ Description
Registration Name
</th>
<th>
Lifetime
</th>
</tr>
</thead>
@@ -79,6 +84,11 @@ The configurations that are exported from `medusa-config.js`.
`configModule`
</td>
<td>
\-
</td>
</tr>
@@ -97,6 +107,11 @@ Services that extend the `TransactionBaseService` class.
Each service is registered under its camel-case name. For example, the `ProductService` is registered as `productService`.
</td>
<td>
Core services by default have the `SINGLETON` lifetime. However, some have a different lifetime which is indicated in this table. Custom services, including services in plugins, by default have the `TRANSIENT` lifetime, unless defined differently within the custom service.
</td>
</tr>
@@ -115,6 +130,11 @@ An instance of Typeorm’s Entity Manager.
`manager`
</td>
<td>
\-
</td>
</tr>
@@ -133,45 +153,60 @@ An instance of Medusa CLI’s logger. You can use it to log messages to the term
`logger`
</td>
<td>
\-
</td>
</tr>
<tr>
<td>
Single Payment Provider
Single Payment Processor
</td>
<td>
An instance of every payment provider that extends the `AbstractPaymentService` class.
An instance of every payment processor that extends the `AbstractPaymentService` or the `AbstractPaymentProcessor` classes.
</td>
<td>
Every payment provider is registered under two names:
Every payment processor is registered under two names:
- Its camel-case name. For example, the `StripeProviderService` is registered as `stripeProviderService`.
- Its camel-case name of the processor. For example, the `StripeProviderService` is registered as `stripeProviderService`.
- `pp_` followed by its identifier. For example, the `StripeProviderService` is registered as `pp_stripe`.
</td>
<td>
By default, it's `TRANSIENT` unless defined differently within the payment processor service.
</td>
</tr>
<tr>
<td>
All Payment Providers
All Payment Processors
</td>
<td>
An array of all payment providers that extend the `AbstractPaymentService` class.
An array of all payment processor that extend the `AbstractPaymentService` or `AbstractPaymentProcessor` class.
</td>
<td>
`paymentProviders`
</td>
<td>
`paymentProviders` is `TRANSIENT`, and each item in it is `TRANSIENT`.
</td>
</tr>
@@ -193,6 +228,11 @@ Every fulfillment provider is registered under two names:
- Its camel-case name. For example, the `WebshipperFulfillmentService` is registered as `webshipperFulfillmentService`.
- `fp_` followed by its identifier. For example, the `WebshipperFulfillmentService` is registered as `fp_webshipper`.
</td>
<td>
By default, it's `SINGLETON` unless defined differently within the fulfillemnt provider service.
</td>
</tr>
@@ -211,6 +251,11 @@ An array of all fulfillment providers that extend the `FulfillmentService` class
`fulfillmentProviders`
</td>
<td>
`fulfillmentProviders` is `TRANSIENT`, and each item in it is `TRANSIENT`.
</td>
</tr>
@@ -232,6 +277,11 @@ Every notification provider is registered under two names:
- Its camel-case name. For example, the `SendGridService` is registered as `sendGridService`.
- `noti_` followed by its identifier. For example, the `SendGridService` is registered as `noti_sendgrid`.
</td>
<td>
By default, it's `SINGLETON` unless defined differently within the notification provider service.
</td>
</tr>
@@ -250,6 +300,11 @@ An array of all notification providers that extend the `AbstractNotificationServ
`notificationProviders`
</td>
<td>
`notificationProviders` is `TRANSIENT`, and each item in it is `TRANSIENT`.
</td>
</tr>
@@ -271,6 +326,11 @@ The file service is registered under two names:
- Its camel-case name. For example, the `MinioService` is registered as `minioService`.
- `fileService`
</td>
<td>
By default, it's `TRANSIENT` unless defined differently within the file service.
</td>
</tr>
@@ -292,6 +352,11 @@ The search service is registered under two names:
- Its camel-case name. For example, the `AlgoliaService` is registered as `algoliaService`.
- `searchService`
</td>
<td>
By default, it's `TRANSIENT` unless defined differently within the search service.
</td>
</tr>
@@ -313,6 +378,11 @@ The tax provider is registered under two names:
- Its camel-case name.
- `tp_` followed by its identifier.
</td>
<td>
By default, it's `SINGLETON` unless defined differently within the tax provider service.
</td>
</tr>
@@ -331,6 +401,11 @@ An array of every tax provider that extends the `AbstractTaxService` class.
`taxProviders`
</td>
<td>
`taxProviders` is `TRANSIENT`, and each item in it is `TRANSIENT`.
</td>
</tr>
@@ -349,6 +424,11 @@ An instance of every service that extends the `OauthService` class.
Each Oauth Service is registered under its camel-case name followed by `Oauth`.
</td>
<td>
By default, it's `TRANSIENT` unless defined differently within the Oauth service.
</td>
</tr>
@@ -367,6 +447,11 @@ An instance of the `FlagRouter`. This can be used to list feature flags, set a f
`featureFlagRouter`
</td>
<td>
\-
</td>
</tr>
@@ -385,6 +470,11 @@ An instance of the Redis client. If Redis is not configured, a fake Redis client
`redisClient`
</td>
<td>
\-
</td>
</tr>
@@ -403,6 +493,11 @@ An instance of every entity.
Each entity is registered under its camel-case name followed by Model. For example, the `CustomerGroup` entity is stored under `customerGroupModel`.
</td>
<td>
\-
</td>
</tr>
@@ -421,6 +516,11 @@ An array of all database entities that is passed to Typeorm when connecting to t
`db_entities`
</td>
<td>
\-
</td>
</tr>
@@ -439,6 +539,11 @@ An instance of each repository.
Each repository is registered under its camel-case name. For example, `CustomerGroupRepository` is stored under `customerGroupRepository`.
</td>
<td>
\-
</td>
</tr>
@@ -461,6 +566,11 @@ Each batch job strategy is registered under three names:
- `batch_` followed by its identifier. For example, the `ProductImportStrategy` is registered under `batch_product-import-strategy`.
- `batchType_` followed by its batch job type. For example, the `ProductImportStrategy` is registered under `batchType_product-import`.
</td>
<td>
\-
</td>
</tr>
@@ -479,6 +589,11 @@ An array of all classes extending the `AbstractBatchJobStrategy` abstract class.
`batchJobStrategies`
</td>
<td>
\-
</td>
</tr>
@@ -497,6 +612,11 @@ An instance of the class implementing the `ITaxCalculationStrategy` interface.
`taxCalculationStrategy`
</td>
<td>
\-
</td>
</tr>
@@ -515,6 +635,11 @@ An instance of the class extending the `AbstractCartCompletionStrategy` class.
`cartCompletionStrategy`
</td>
<td>
\-
</td>
</tr>
@@ -533,6 +658,11 @@ An instance of the class implementing the `IPriceSelectionStrategy` interface.
`priceSelectionStrategy`
</td>
<td>
\-
</td>
</tr>
@@ -551,6 +681,11 @@ An instance of strategies that aren’t of the specific types mentioned above an
Its camel-case name.
</td>
<td>
\-
</td>
</tr>
</tbody>
@@ -576,7 +711,7 @@ Please note that in endpoints some resources, such as repositories, are not avai
### In Classes
In classes such as services, strategies, or subscribers, you can load resources in the constructor function. The constructor receives an object of dependencies as a first parameter. Each dependency in the object should use the registration name of the resource that should be injected to the class.
In classes such as services, strategies, or subscribers, you can load resources in the constructor function using dependency injection. The constructor receives an object of dependencies as a first parameter. Each dependency in the object should use the registration name of the resource that should be injected to the class.
For example:
+305
View File
@@ -0,0 +1,305 @@
---
description: 'Learn how to create a module and test the module in your Medusa backend.'
---
import DocCardList from '@theme/DocCardList';
import Icons from '@theme/Icon';
# How to Create a Module
In this document, you’ll learn how to create a module and test the module in your Medusa backend.
This document covers the general steps of creating an internal module, but does not explore actually implementing any functionality in it. An internal module is a TypeScript/JavaScript module that is loaded by the Medusa backend as part of the commerce application to extend or replace a functionality within it, such as the cache or event bus functionality.
---
## (Optional) Step 0: Project Preparation
Before you start implementing the custom functionality in your module, it's recommended to create a directory that holds your module and prepare the following structure in it:
```
custom-module
|
|___ index.ts
|
|___ services // directory
```
The directory can be an NPM project, but that is optional. `index.ts` acts as an entry point to your Module. You'll learn about its content in a later step. The `service` directory will hold your custom services. If you're adding other resources you can add other directories for them. For example, if you're adding an entity you can add a `models` directory.
:::tip
You can use JavaScript instead of TypeScript.
:::
It's also recommended to use the following TypeScript config in `tsconfig.json`, which should be added in the root of the project holding your module:
```json
{
"compilerOptions": {
"lib": [
"es2020"
],
"target": "2020",
"outDir": "./dist",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
},
"include": ["src"],
"exclude": [
"dist",
"./src/**/__tests__",
"./src/**/__mocks__",
"./src/**/__fixtures__",
"node_modules"
]
}
```
---
## Step 1: Implement the Custom Functionality
This step depends on what you’re actually implementing. For example, you can implement the cache or events module, or you can implement a commerce module. If what you’re creating has a guide, you can refer to it while implementing the functionalities.
### Note About Project Structure
When developing your module, it's important to note that you'll later be referencing the module using a file path to an `index.ts` or `index.js` file. This file is explained in the next step and acts as an entry point and a definition of your module.
So, make sure when implementing your module you take into account that the module should be easily referenced from your local Medusa server. For example, you can develop your module in a sibling directory of the Medusa backend that you can reference with `../custom-module`.
Keep in mind that when you publish the module to NPM, you'll have to move your module into a new NPM project. This is covered in the [Publish Module documentation](./publish.md).
### Recommended Guides
<DocCardList colSize={6} items={[
{
type: 'link',
href: '/development/cache/create',
label: 'Create a Cache Module',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to create a cache module in Medusa.',
}
},
{
type: 'link',
href: '/development/events/create-module',
label: 'Create an Events Module',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to create an events module in Medusa.',
}
},
]} />
---
### Step 2: Export Module
After implementing the module, you must export a module object that helps the Medusa backend understand how to use this Module. This is done in the file `index.ts`.
The file must export an object with the following properties:
```ts
type ModuleExports = {
service: Constructor<any>
loaders?: ModuleLoaderFunction[]
migrations?: any[]
models?: Constructor<any>[]
runMigrations?(
options: LoaderOptions,
moduleDeclaration: InternalModuleDeclaration
): Promise<void>
revertMigration?(
options: LoaderOptions,
moduleDeclaration: InternalModuleDeclaration
): Promise<void>
}
```
:::tip
All property types such as `ModuleLoaderFunction` can be loaded from the `@medusajs/modules-sdk` package.
:::
Where:
- `service`: This is the only required property to be exported. It should be the main service your module exposes, and it must implement all the declared methods on the module interface. For example, if it's a cache module, it must implement the `ICacheService` interface exported from `@medusajs/types`.
- `loaders`: (optional) an array of functions used to perform an action while loading the module. For example, you can log a message that the module has been loaded, or if your module's scope is [isolated](#module-scope) you can use the loader to establish a database connection.
- `migrations`: (optional) an array of objects containing database migrations that should run when the `migration` command is used with Medusa's CLI.
- `models`: (optional) an array of entities that your module creates.
- `runMigrations`: (optional) a function that can be used to define migrations to run when the `migration run` command is used with Medusa's CLI. The migrations will only run if they haven't already. This will only be executed if the module's scope is [isolated](#module-scope).
- `revertMigration`: (optional) a function can be used to define how migrations should be reverted when the `migration revert` command is used with Medusa's CLI. This will only be executed if the module's scope is [isolated](#module-scope).
Here's an example implementation of `index.ts` from Medusa's Redis Cache module:
```ts title=index.ts
import { ModuleExports } from "@medusajs/modules-sdk"
import Loader from "./loaders"
import { RedisCacheService } from "./services"
const service = RedisCacheService
const loaders = [Loader]
const moduleDefinition: ModuleExports = {
service,
loaders,
}
export default moduleDefinition
```
---
## Step 3: Reference Module
To use your module in the Medusa backend, add your module to `medusa-config.js`:
```js title=medusa-config.js
module.exports = {
// ...
modules: {
// ...
moduleType: {
resolve: "module-name-or-path",
options: {
// options if necessary
},
// optional
resources: "shared",
},
},
}
```
The way you add your module depends on its type and what options it requires, if any. Note that in the above code example:
- `moduleType` is the type of your module. For example, if your module is a cache module, it should be changed to `cacheService`.
- `resolve` is used to reference the Module. Its value should be either the name of an NPM package module, or a relative file path to the module. This is explained more in the [Module Reference section](#module-reference).
- `options` should hold any options of your module, if necessary.
- `resources` is an optional property that indicates whether the module shares the same dependency container as the rest of the resources in the Medusa backend. More details are explained in the [Module Scope section](#module-scope).
### Module Reference
When the module is installed as an NPM package, the value of the `resolve` property should be the name of that package. For example:
```js title=medusa-config.js
module.exports = {
// ...
modules: {
// ...
moduleType: {
resolve: "custom-module",
// ...
},
},
}
```
However, when using a local module, you must reference the module using a relative file path to it from the Medusa backend.
For example, consider you have the following file structure:
```
|
|___ custom-module
| |
| |___ index.ts
| |
| |___ services
| | |
| | |___ custom-service.ts
| |___ // more files
|
|
|___ medusa-backend
```
You can reference your module in two ways:
1\. Referencing the directory: In this case, it's assumed that the `index.ts` file that contains the module definition is in the root of the directory you referenced. Using the above example, the file path would be in this case:
```js title=medusa-config.js
module.exports = {
// ...
modules: {
// ...
moduleType: {
resolve: "../custom-module",
// ...
},
},
}
```
2\. Referencing `index` file: In this case, it's assumed that the `index.ts` or `index.js` file you're referencing includes the module definition. Using the above example, the file path would be in this case:
```js title=medusa-config.js
module.exports = {
// ...
modules: {
// ...
moduleType: {
resolve: "../custom-module/index.ts",
// ...
},
},
}
```
### Module Scope
By default, the module shares the same dependency container used across the Medusa backend. So, the module can benefit from the core services and other resources available through [dependency injection](../fundamentals/dependency-injection.md). The module can also benefit from the same database connection.
The module's scope can be changed using the `resources` property available as part of the module's configurations:
```js title=medusa-config.js
module.exports = {
// ...
modules: {
// ...
moduleType: {
// other configurations
resources: "shared",
},
},
}
```
The `resources` property can have one of the following values:
- `shared`: (default) The dependency container is shared with the module, including the database connection. You don't need to establish the database connection yourself in a loader.
- `isolated`: the module receives an empty dependency container, and only its own dependencies will be registered in the container. When using this value, you must establish the database connection yourself and managing other resources within your module.
---
## Step 4: Test Your Module
Finally, to test your module, run the following command:
```bash npm2yarn
npm run start
```
This starts the Medusa backend and runs your module as part of it.
---
## Next Steps
After you finish developing your module, you can publish it as an NPM package with [this guide](./publish.md).
@@ -0,0 +1,47 @@
---
description: 'Learn what Modules are and how can you use them during your custom development with Medusa.'
---
import DocCardList from '@theme/DocCardList';
import Icons from '@theme/Icon';
# Modules
In this document, you’ll learn what Modules are and how can you use them during your custom development with Medusa.
## Overview
Modules are self-contained, reusable pieces of code that encapsulate specific functionality or features within an ecommerce application. They foster separation of concerns, maintainability, and reusability by organizing code into smaller, independent units that can be easily managed, tested, and integrated with other modules.
Modules further increase Medusa’s extensibility. Commerce modules, such as the cart engine, can be extended or entirely replaced with your own custom logic. They can also run independently of the core Medusa package, allowing you to utilize the commerce module within a larger commerce ecosystem. For example, you can use the Order module as an Order Management System (OMS) without using Medusa’s core.
This also applies to core logic such as caching or events systems. You can use modules to integrate any logic or third-party service to handle this logic. This gives you greater flexibility in how you choose your tech stack.
Modules are created and loaded similarly to plugins. They can be loaded from a local project, or they can be installed and loaded from an NPM package. In the Medusa backend, they’re added as part of the configurations in `medusa-config.js` to use and load them within the backend.
---
## Custom Development
Developers can create their own modules and use them in their Medusa backend. They can also publish these modules to NPM to reuse them across Medusa backend or allow other developers to use them.
<DocCardList colSize={6} items={[
{
type: 'link',
href: '/development/modules/create',
label: 'Create a Module',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to create a module in Medusa.'
}
},
{
type: 'link',
href: '/development/modules/publish',
label: 'Publish Module',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to publish your module on NPM.'
}
},
]} />
+198
View File
@@ -0,0 +1,198 @@
---
description: 'Learn how to prepare and publish your custom module to NPM, then how to install it in the Medusa backend.'
---
# How to Publish a Module
In this document, you'll learn how to prepare and publish your custom module to NPM, then how to install it in the Medusa backend.
## Prerequisites
This guide assumes you've already created a custom module. If not, follow [this guide](./create.mdx) first to create a module.
You also need an [NPM account](https://www.npmjs.com/signup) to publish the module with.
---
## Step 1: Create an NPM Project
If your module isn't located in an NPM project already, you must create one first that will hold your module.
To do that, run the following commands to create a directory and initialize an NPM project in it:
```bash npm2yarn
mkdir my-module
npm init
```
You’ll be asked a couple of questions related to your package, such as its name or license. You can keep the default for now or set them right away.
Once you’re done, you should have a `package.json` created in the directory.
---
## Step 2: Changes to package.json
In your `package.json` file, add or update the following fields:
```json title=package.json
{
// other fields
"main": "dist/index.js",
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"devDependencies": {
"@medusajs/types": "^0.0.2",
"cross-env": "^5.2.1",
"typescript": "^4.4.4"
},
"scripts": {
"watch": "tsc --build --watch",
"prepare": "cross-env NODE_ENV=production npm run build",
"build": "tsc --build",
},
"dependencies": {
"@medusajs/modules-sdk": "^0.1.0",
}
}
```
This adds the necessary dependencies for development and publishing, including the `@medusajs/modules-sdk` package. It also adds the following scripts:
- `build`: can be used to manually build your module.
- `prepare`: can be used to prepare your module for publishing on NPM
- `watch`: (optional, for development) can be used to re-build your module whenever any changes occur without having to manually trigger the `build`.
---
## Step 3: Configure tsconfig.json
If you don't already have a `tsconfig.json` file, create one in the root of your NPM project with the following content:
```json title=tsconfig.json
{
"compilerOptions": {
"lib": [
"es2020"
],
"target": "2020",
"outDir": "./dist",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
},
"include": ["src"],
"exclude": [
"dist",
"./src/**/__tests__",
"./src/**/__mocks__",
"./src/**/__fixtures__",
"node_modules"
]
}
```
This allows you to use the recommended TypeScript configurations and sets the output directory to `dist`. This is essential for preparing your module for publishing.
---
## Step 4: Change Module Structure
To ensure that the files are built from the `src` directory to the `dist` directory, make sure to move the module content to a `src` directory inside the new NPM project.
---
## Step 5: Publish and Use Module
This section explains how to publish your module to NPM.
### Run Prepare Command
Before you publish or update your module, make sure to run the `prepare` command defined earlier:
```bash npm2yarn
npm run prepare
```
### Login
In your terminal, log in with your NPM account:
```bash
npm login
```
You’ll be asked to enter your NPM email and password.
### Publish Module Package
Once you’re logged in, you can publish your package with the following command:
```bash
npm publish
```
Your package is then published on NPM and everyone can use it and install it.
### Install Module
To install your published module, you can run the following command on any Medusa backend project:
```bash
npm install module-name
```
Where `module-name` is the name of your module.
### Add Module to medusa-config.js
In `medusa-config.js` on your Medusa backend, add your module to the exported configurations:
```js title=medusa-config.js
module.exports = {
// ...
modules: {
// ...
moduleType: {
resolve: "<module-name>",
options: {
// options if necessary
},
},
},
}
```
Where `<module-name>` is the name of your NPM package.
You can learn more about the available options in the [Create Module documentation](./create.mdx#step-3-reference-module).
### Update Module
To update your module at a later point, you can run the following command to change the NPM version:
```bash
npm version <type>
```
Where `<type>` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. You can see the [full list of types in NPM’s documentation](https://docs.npmjs.com/cli/v8/commands/npm-version).
Then, publish the new update:
```bash
npm publish
```
@@ -15,9 +15,7 @@ If you’re unfamiliar with the Notification architecture in Medusa, it is recom
## Prerequisites
Before you start creating a Notification Provider, you need to either install a [Medusa backend](../backend/install.mdx), or create it in a [plugin](../plugins/overview.mdx).
You also need to [setup Redis](../backend/prepare-environment.mdx#redis) and [configure it with the Medusa backend](../backend/configurations.md#redis) to test out the Notification provider.
Before you start creating a Notification Provider, you need to either install a [Medusa backend](../backend/install.mdx), or create it in a [plugin](../plugins/overview.mdx). The Medusa backend must also have an event bus module installed, which is available when using the default Medusa backend starter.
---
@@ -297,7 +295,7 @@ Notice that the value of the `identifier` static property defined in the `EmailS
## Test Sending Notifications with your Notification Provider
Make sure you've configured Redis with your Medusa backend as explained in the [Prerequisites](#prerequisites) section and that the Redis service is running.
Make sure you have an event bus module configured in your Medusa backend. You can learn more on how to do that in the [Configurations guide](../backend/configurations.md).
Then, start by running your Medusa backend:
@@ -15,7 +15,7 @@ Medusa was built with flexibility and extendibility in mind. All different compo
Developers can use plugins to take advantage of this abstraction, flexibility, and extendibility. Plugins allow developers to implement custom features or integrate third-party services into Medusa.
For example, if you want to use Stripe as a payment provider in your store, then you can install the Stripe plugin on your backend and use it.
For example, if you want to use Stripe as a payment processor in your store, then you can install the Stripe plugin on your backend and use it.
An alternative approach is developing a custom way of handling payment on your ecommerce store. Both approaches are achievable by either creating a plugin or using an existing plugin.
@@ -19,11 +19,7 @@ This guide explains how to create a scheduled job on your Medusa backend. The sc
### Medusa Components
It is assumed that you already have a Medusa backend installed and set up. If not, you can follow the [quickstart guide](../backend/install.mdx) to get started.
### Redis
Redis is required for scheduled jobs to work. Make sure you [install Redis](../../development/backend/prepare-environment.mdx#redis) and [configure it with your Medusa backend](../../development/backend/configurations.md#redis).
It is assumed that you already have a Medusa backend installed and set up. If not, you can follow the [quickstart guide](../backend/install.mdx) to get started. The Medusa backend must also have an event bus module installed, which is available when using the default Medusa backend starter.
---
@@ -15,7 +15,7 @@ Scheduled jobs (also known as cron jobs) are tasks performed at a specific time
For example, you can synchronize your inventory with an Enterprise Resource Planning (ERP) system once a day using a scheduled job.
In the Medusa Backend, the scheduled jobs queue is implemented using [Redis](https://redis.io/) and [Bull](https://www.npmjs.com/package/bull). So, for scheduled jobs to work, you must have [Redis enabled](../../development/backend/configurations.md#redis).
In the Medusa Backend, the scheduled jobs queue is implemented using [Redis](https://redis.io/) and [Bull](https://www.npmjs.com/package/bull). So, for scheduled jobs to work, you must have [Redis installed and enabled](../../development/backend/configurations.md#redis).
:::tip
@@ -3,13 +3,13 @@ description: 'Learn how to create a service in Medusa. This guide also includes
addHowToData: true
---
# Create a Service
# How to Create a Service
In this document, you’ll learn how you can create a [Service](./overview.mdx) and use it across your Medusa backend just like any of the core services.
## Implementation
## Service Implementation
To create a service, create a TypeScript or JavaScript file in `src/services` to hold the service. The name of the file should be the registration name of the service without `Service` as it will be appended to it by default.
To create a service, create a TypeScript or JavaScript file in `src/services` to hold the service. The name of the file should be the name of the service without `Service`. This is essential as the file name is used when registering the service in the [dependency container](../fundamentals/dependency-injection.md), and `Service` is appended to the camel-case version of the file name automatically.
For example, if you want to create a service `helloService`, create the file `hello.ts` in `src/services` with the following content:
@@ -28,6 +28,33 @@ class HelloService extends TransactionBaseService {
export default HelloService
```
This service will be registered in the dependency container as `helloService`.
---
## Service Life Time
As the dependency container in Medusa is built on top of [awilix](https://github.com/jeffijoe/awilix), you can specify the [Lifetime](https://github.com/jeffijoe/awilix#lifetime-management) of a service. The lifetime is added as a static property to the service.
There are three lifetime types:
1. `Lifetime.TRANSIENT`: (default for custom services) when used, a new instance of the service is created everytime it is resolved in other resources from the dependency container.
2. `Lifetime.SCOPED`: when used, an instance of the service is created and reused in the scope of the dependency container. So, when the service is resolved in other resources that share that dependency container, the same instance of the service will be returned.
3. `Lifetime.SINGLETON`: (default for core services) when used, the service is always reused, regardless of the scope. An instance of the service is cached in the root container.
You can set the lifetime of your service by setting the `LIFE_TIME` static property:
```ts title=/src/services/hello.ts
import { TransactionBaseService } from "@medusajs/medusa"
import { Lifetime } from "awilix"
class HelloService extends TransactionBaseService {
static LIFE_TIME = Lifetime.SCOPED
// ...
}
```
---
## Service Constructor
@@ -0,0 +1,86 @@
---
description: 'Learn how to create a service in Medusa. This guide also includes how to use services in other services, subscribers, and endpoints.'
addHowToData: true
---
# How to Extend a Service
In this document, you’ll learn how to extend a core service in Medusa.
## Overview
Medusa’s core services cover a wide range of functionalities related to each domain or entity. You can extend these services to add custom methods or override existing methods.
### Word of Caution about Overriding
Extending services to add new methods shouldn't cause any issues within your commerce application. However, if you extend them to override their existing methods, you should be aware that this could have negative implications, such as unanticipated bugs, especially when you try to upgrade the core Medusa package to a newer version.
---
## Step 1: Create the Service File
In your Medusa backend, create the file `src/services/product.ts`. This file will hold your extended service.
Note that the name of the file must be the same as the name of the original service in the core package. So, if you’re extending the `ProductService`, the file’s name should be `product.ts`. On the other hand, if you’re extending the `CustomerService`, the file’s name should be `customer.ts`.
---
## Step 2: Implementing the Service
In the file, you can import the original service from the Medusa core, then create your service that extends the core service.
For example, to extend the Product service:
```ts title=src/services/product.ts
import {
ProductService as MedusaProductService,
} from "@medusajs/medusa"
class ProductService extends MedusaProductService {
// TODO add customizations
}
export default ProductService
```
Notice that you alias the `ProductService` of the core to avoid naming conflicts.
Within the service, you can add new methods or extend existing ones.
You can also change the lifetime of the service:
```ts title=src/services/product.ts
import { Lifetime } from "awilix"
import {
ProductService as MedusaProductService,
} from "@medusajs/medusa"
class ProductService extends MedusaProductService {
// The default life time for a core service is SINGLETON
static LIFE_TIME = LifeTime.SCOPED
// ...
}
export default ProductService
```
You can learn more details about the service lifetime and other considerations when creating a service in the [Create Service documentation](./create-service.md).
---
## Step 3: Test it Out
To test out your customization, start by transpiling your files by running the following command in the root directory of the Medusa backend:
```bash npm2yarn
npm run build
```
Then, start the backend:
```bash npm2yarn
npm run start
```
You should see the customizations you made in effect.
+21 -7
View File
@@ -2,7 +2,7 @@
description: 'Learn what Services are in Medusa. Services represent bundled helper methods that you want to use across your commerce application.'
---
import DocCard from '@theme/DocCard';
import DocCardList from '@theme/DocCardList';
import Icons from '@theme/Icon';
# Services
@@ -17,11 +17,15 @@ For example, you can use Medusa’s `productService` to get the list of products
In the Medusa backend, custom services are TypeScript or JavaScript files located in the `src/services` directory. Each service should be a class that extends the `TransactionBaseService` class from the core Medusa package `@medusajs/medusa`. Each file you create in `src/services` should hold one service and export it.
The file name is important as it determines the name of the service when you need to use it elsewhere. The name of the service will be registered as the camel-case version of the file name + `Service` at the end of the name.
The file name is important as it determines the name of the service when you need to use it elsewhere. The name of the service will be registered in the dependency container as the camel-case version of the file name with `Service` appended to the end of the name. Other resources, such as other services or endpoints, will use that name when resolving the service from the dependency container.
For example, if the file name is `hello.ts`, the service will be registered as `helloService`. If the file name is `hello-world.ts`, the service name will be registered as `helloWorldService`.
For example, if the file name is `hello.ts`, the service will be registered as `helloService` in the dependency container. If the file name is `hello-world.ts`, the service name will be registered as `helloWorldService`.
The registration name of the service is important, as you’ll be referring to it when you want to get access to the service using dependency injection or in routes.
:::note
You can learn more about the dependency container and how it works in the [dependency injection](../fundamentals/dependency-injection.md) documentation.
:::
The service must then be transpiled using the `build` command, which moves them to the `dist` directory, to be used across your commerce application.
@@ -37,7 +41,8 @@ If you're creating a service in a plugin, learn more about the required structur
Developers can create custom services in the Medusa backend, a plugin, or in a Commerce Module.
<DocCard item={{
<DocCardList colSize={6} items={[
{
type: 'link',
href: '/development/services/create-service',
label: 'Create a Service',
@@ -45,5 +50,14 @@ Developers can create custom services in the Medusa backend, a plugin, or in a C
icon: Icons['academic-cap-solid'],
description: 'Learn how to create a service in Medusa.'
}
}}
/>
},
{
type: 'link',
href: '/development/services/extend-service',
label: 'Extend a Service',
customProps: {
icon: Icons['academic-cap-solid'],
description: 'Learn how to extend a core Medusa service.'
}
},
]} />