docs: revise main docs outline (#10502)

This commit is contained in:
Shahed Nasser
2024-12-09 13:54:42 +02:00
committed by GitHub
parent c8cb9b5c1a
commit 0ae98c51eb
141 changed files with 814 additions and 1181 deletions
@@ -0,0 +1,33 @@
export const metadata = {
title: `${pageNumber} Architectural Modules`,
}
# {metadata.title}
In this chapter, youll learn about architectural modules.
## What is an Architectural Module?
An architectural module implements features and mechanisms related to the Medusa applications architecture and infrastructure.
Since modules are interchangeable, you have more control over Medusas architecture. For example, you can choose to use Memcached for event handling instead of Redis.
---
## Architectural Module Types
There are different architectural module types including:
![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/architectural-modules_bj9bb9.jpg)
- Cache Module: Defines the caching mechanism or logic to cache computational results.
- Event Module: Integrates a pub/sub service to handle subscribing to and emitting events.
- Workflow Engine Module: Integrates a service to store and track workflow executions and steps.
- File Module: Integrates a storage service to handle uploading and managing files.
- Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers.
---
## Architectural Modules List
Refer to the [Architectural Modules reference](!resources!/architectural-modules) for a list of Medusas architectural modules, available modules to install, and how to create an architectural module.
@@ -0,0 +1,55 @@
export const metadata = {
title: `${pageNumber} Commerce Modules`,
}
# {metadata.title}
In this chapter, you'll learn about Medusa's commerce modules.
## What is a Commerce Module?
Commerce modules are built-in [modules](../page.mdx) of Medusa that provide core commerce logic specific to domains like Products, Orders, Customers, Fulfillment, and much more.
Medusa's commerce modules are used to form Medusa's default [workflows](!resources!/medusa-workflows-reference) and [APIs](!api!/store). For example, when you call the add to cart endpoint. the add to cart workflow runs which uses the Product Module to check if the product exists, the Inventory Module to ensure the product is available in the inventory, and the Cart Module to finally add the product to the cart.
<Note title="Tip">
You'll find the details and steps of the add-to-cart workflow in [this workflow reference](!resources!/references/medusa-workflows/addToCartWorkflow)
</Note>
The core commerce logic contained in Commerce Modules is also available directly when you are building customizations. This granular access to commerce functionality is unique and expands what's possible to build with Medusa drastically.
### List of Medusa's Commerce Modules
Refer to [this reference](!resources!/commerce-modules) for a full list of commerce modules in Medusa.
---
## Use Commerce Modules in Custom Flows
Similar to your [custom modules](../page.mdx), the Medusa application registers a commerce module's service in the [container](../../medusa-container/page.mdx). So, you can resolve it in your custom flows. This is useful as you build unique requirements extending core commerce features.
For example, consider you have a [workflow](../../../fundamentals/workflows/page.mdx) (a special function that performs a task in a series of steps with rollback mechanism) that needs a step to retrieve the total number of products. You can create a step in the workflow that resolves the Product Module's service from the container to use its methods:
export const highlights = [
["6", `"product"`, "Resolve the Product Module's service from the container."],
["8", "listAndCountProducts", "Use the service's method to get the products count."]
]
```ts highlights={highlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
export const countProductsStep = createStep(
"count-products",
async ({ }, { container }) => {
const productModuleService = container.resolve("product")
const [,count] = await productModuleService.listAndCountProducts()
return new StepResponse(count)
}
)
```
Your workflow can use services of both custom and commerce modules, supporting you in building custom flows without having to re-build core commerce features.
@@ -0,0 +1,68 @@
export const metadata = {
title: `${pageNumber} Module Container`,
}
# {metadata.title}
In this chapter, you'll learn about the module's container and how to resolve resources in that container.
Since modules are isolated, each module has a local container only used by the resources of that module.
So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container.
### List of Registered Resources
Find a list of resources or dependencies registered in a module's container in [this Development Resources reference](!resources!/medusa-container-resources).
---
## Resolve Resources
### Services
A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container.
For example:
```ts highlights={[["4"], ["10"]]}
import { Logger } from "@medusajs/framework/types"
type InjectedDependencies = {
logger: Logger
}
export default class HelloModuleService {
protected logger_: Logger
constructor({ logger }: InjectedDependencies) {
this.logger_ = logger
this.logger_.info("[HelloModuleService]: Hello World!")
}
// ...
}
```
### Loader
A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources.
For example:
```ts highlights={[["9"]]}
import {
LoaderOptions,
} from "@medusajs/framework/types"
import {
ContainerRegistrationKeys,
} from "@medusajs/framework/utils"
export default async function helloWorldLoader({
container,
}: LoaderOptions) {
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
logger.info("[helloWorldLoader]: Hello, World!")
}
```
@@ -0,0 +1,465 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `${pageNumber} Perform Database Operations in a Service`,
}
# {metadata.title}
In this chapter, you'll learn how to perform database operations in a module's service.
<Note>
This chapter is intended for more advanced database use-cases where you need more control over queries and operations. For basic database operations, such as creating or retrieving data of a model, use the [Service Factory](../service-factory/page.mdx) instead.
</Note>
## Run Queries
[MikroORM's entity manager](https://mikro-orm.io/docs/entity-manager) is a class that has methods to run queries on the database and perform operations.
Medusa provides an `InjectManager` decorator imported from `@medusajs/utils` that injects a service's method with a [forked entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager).
So, to run database queries in a service:
1. Add the `InjectManager` decorator to the method.
2. Add as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator imported from `@medusajs/utils`. This context holds database-related context, including the manager injected by `InjectManager`
For example, in your service, add the following methods:
export const methodsHighlight = [
["12", "getCount", "Retrieves the number of records in `my_custom` using the `count` method."],
["19", "getCountSql", "Retrieves the number of records in `my_custom` using the `execute` method."]
]
```ts highlights={methodsHighlight}
// other imports...
import {
InjectManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { SqlEntityManager } from "@mikro-orm/knex"
class HelloModuleService {
// ...
@InjectManager()
async getCount(
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<number> {
return await sharedContext.manager.count("my_custom")
}
@InjectManager()
async getCountSql(
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<number> {
const data = await sharedContext.manager.execute(
"SELECT COUNT(*) as num FROM my_custom"
)
return parseInt(data[0].num)
}
}
```
You add two methods `getCount` and `getCountSql` that have the `InjectManager` decorator. Each of the methods also accept the `sharedContext` parameter which has the `MedusaContext` decorator.
The entity manager is injected to the `sharedContext.manager` property, which is an instance of [EntityManager from the @mikro-orm/knex package](https://mikro-orm.io/api/5.9/knex/class/EntityManager).
You use the manager in the `getCount` method to retrieve the number of records in a table, and in the `getCountSql` to run a PostgreSQL query that retrieves the count.
<Note>
Refer to [MikroORM's reference](https://mikro-orm.io/api/5.9/knex/class/EntityManager) for a full list of the entity manager's methods.
</Note>
---
## Execute Operations in Transactions
To wrap database operations in a transaction, you create two methods:
1. A private or protected method that's wrapped in a transaction. To wrap it in a transaction, you use the `InjectTransactionManager` decorator imported from `@medusajs/utils`.
2. A public method that calls the transactional method. You use on it the `InjectManager` decorator as explained in the previous section.
Both methods must accept as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator imported from `@medusajs/utils`. It holds database-related contexts passed through the Medusa application.
For example:
export const opHighlights = [
["11", "InjectTransactionManager", "A decorator that injects the a transactional entity manager into the `sharedContext` parameter."],
["17", "MedusaContext", "A decorator to use Medusa's shared context."],
["20", "nativeUpdate", "Update a record."],
["31", "execute", "Retrieve the updated record."],
["38", "InjectManager", "A decorator that injects a forked entity manager into the context."],
]
```ts highlights={opHighlights}
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class HelloModuleService {
// ...
@InjectTransactionManager()
protected async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
const transactionManager = sharedContext.transactionManager
await transactionManager.nativeUpdate(
"my_custom",
{
id: input.id,
},
{
name: input.name,
}
)
// retrieve again
const updatedRecord = await transactionManager.execute(
`SELECT * FROM my_custom WHERE id = '${input.id}'`
)
return updatedRecord
}
@InjectManager()
async update(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
) {
return await this.update_(input, sharedContext)
}
}
```
The `HelloModuleService` has two methods:
- A protected `update_` that performs the database operations inside a transaction.
- A public `update` that executes the transactional protected method.
The shared context's `transactionManager` property holds the transactional entity manager (injected by `InjectTransactionManager`) that you use to perform database operations.
<Note>
Refer to [MikroORM's reference](https://mikro-orm.io/api/5.9/knex/class/EntityManager) for a full list of the entity manager's methods.
</Note>
### Why Wrap a Transactional Method
The variables in the transactional method (for example, `update_`) hold values that are uncommitted to the database. They're only committed once the method finishes execution.
So, if in your method you perform database operations, then use their result to perform other actions, such as connecting to a third-party service, you'll be working with uncommitted data.
By placing only the database operations in a method that has the `InjectTransactionManager` and using it in a wrapper method, the wrapper method receives the committed result of the transactional method.
<Note title="Optimization Tip">
This is also useful if you perform heavy data normalization outside of the database operations. In that case, you don't hold the transaction for a longer time than needed.
</Note>
For example, the `update` method could be changed to the following:
```ts
// other imports...
import { EntityManager } from "@mikro-orm/knex"
class HelloModuleService {
// ...
@InjectManager()
async update(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
) {
const newData = await this.update_(input, sharedContext)
await sendNewDataToSystem(newData)
return newData
}
}
```
In this case, only the `update_` method is wrapped in a transaction. The returned value `newData` holds the committed result, which can be used for other operations, such as passed to a `sendNewDataToSystem` method.
### Using Methods in Transactional Methods
If your transactional method uses other methods that accept a Medusa context, pass the shared context to those methods.
For example:
```ts
// other imports...
import { EntityManager } from "@mikro-orm/knex"
class HelloModuleService {
// ...
@InjectTransactionManager()
protected async anotherMethod(
@MedusaContext() sharedContext?: Context<EntityManager>
) {
// ...
}
@InjectTransactionManager()
protected async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
anotherMethod(sharedContext)
}
}
```
You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context.
The `anotherMethod` now runs in the same transaction as the `update_` method.
---
## Configure Transactions
To configure the transaction, such as its [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html), use the `baseRepository` dependency registered in your module's container.
The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more.
The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction.
For example, resolve the `baseRepository` in your service's constructor:
<CodeTabs group="service-type">
<CodeTab label="Extending Service Factory" value="service-factory">
```ts highlights={[["14"]]}
import { MedusaService } from "@medusajs/framework/utils"
import MyCustom from "./models/my-custom"
import { DAL } from "@medusajs/framework/types"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
}
class HelloModuleService extends MedusaService({
MyCustom,
}){
protected baseRepository_: DAL.RepositoryService
constructor({ baseRepository }: InjectedDependencies) {
super(...arguments)
this.baseRepository_ = baseRepository
}
}
export default HelloModuleService
```
</CodeTab>
<CodeTab label="Without Service Factory" value="no-service-factory">
```ts highlights={[["10"]]}
import { DAL } from "@medusajs/framework/types"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
}
class HelloModuleService {
protected baseRepository_: DAL.RepositoryService
constructor({ manager }: InjectedDependencies) {
this.baseRepository_ = baseRepository
}
}
export default HelloModuleService
```
</CodeTab>
</CodeTabs>
Then, add the following method that uses it:
export const repoHighlights = [
["20", "transaction", "Wrap the function parameter in a transaction."]
]
```ts highlights={repoHighlights}
// ...
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
class HelloModuleService {
// ...
@InjectTransactionManager()
protected async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
return await this.baseRepository_.transaction(
async (transactionManager) => {
await transactionManager.nativeUpdate(
"my_custom",
{
id: input.id,
},
{
name: input.name,
}
)
// retrieve again
const updatedRecord = await transactionManager.execute(
`SELECT * FROM my_custom WHERE id = '${input.id}'`
)
return updatedRecord
},
{
transaction: sharedContext.transactionManager,
}
)
}
@InjectManager()
async update(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
) {
return await this.update_(input, sharedContext)
}
}
```
The `update_` method uses the `baseRepository_.transaction` method to wrap a function in a transaction.
The function parameter receives a transactional entity manager as a parameter. Use it to perform the database operations.
The `baseRepository_.transaction` method also receives as a second parameter an object of options. You must pass in it the `transaction` property and set its value to the `sharedContext.transactionManager` property so that the function wrapped in the transaction uses the injected transaction manager.
<Note>
Refer to [MikroORM's reference](https://mikro-orm.io/api/5.9/knex/class/EntityManager) for a full list of the entity manager's methods.
</Note>
### Transaction Options
The second parameter of the `baseRepository_.transaction` method is an object of options that accepts the following properties:
1. `transaction`: Set the transactional entity manager passed to the function. You must provide this option as explained in the previous section.
```ts highlights={[["16"]]}
// other imports...
import { EntityManager } from "@mikro-orm/knex"
class HelloModuleService {
// ...
@InjectTransactionManager()
async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
return await this.baseRepository_.transaction<EntityManager>(
async (transactionManager) => {
// ...
},
{
transaction: sharedContext.transactionManager,
}
)
}
}
```
2. `isolationLevel`: Sets the transaction's [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html). Its values can be:
- `read committed`
- `read uncommitted`
- `snapshot`
- `repeatable read`
- `serializable`
```ts highlights={[["19"]]}
// other imports...
import { IsolationLevel } from "@mikro-orm/core"
class HelloModuleService {
// ...
@InjectTransactionManager()
async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
return await this.baseRepository_.transaction<EntityManager>(
async (transactionManager) => {
// ...
},
{
isolationLevel: IsolationLevel.READ_COMMITTED,
}
)
}
}
```
3. `enableNestedTransactions`: (default: `false`) whether to allow using nested transactions.
- If `transaction` is provided and this is disabled, the manager in `transaction` is re-used.
```ts highlights={[["16"]]}
class HelloModuleService {
// ...
@InjectTransactionManager()
async update_(
input: {
id: string,
name: string
},
@MedusaContext() sharedContext?: Context<EntityManager>
): Promise<any> {
return await this.baseRepository_.transaction<EntityManager>(
async (transactionManager) => {
// ...
},
{
enableNestedTransactions: false,
}
)
}
}
```
@@ -0,0 +1,113 @@
export const metadata = {
title: `${pageNumber} Module Isolation`,
}
# {metadata.title}
In this chapter, you'll learn how modules are isolated, and what that means for your custom development.
<Note title="Summary">
- Modules can't access resources, such as services or data models, from other modules.
- Use Medusa's linking concepts, as explained in the [Module Links chapters](../../../fundamentals/module-links/page.mdx), to extend a module's data models and retrieve data across modules.
</Note>
## How are Modules Isolated?
A module is unaware of any resources other than its own, such as services or data models. This means it can't access these resources if they're implemented in another module.
For example, your custom module can't resolve the Product Module's main service or have direct relationships from its data model to the Product Module's data models.
---
## Why are Modules Isolated
Some of the module isolation's benefits include:
- Integrate your module into any Medusa application without side-effects to your setup.
- Replace existing modules with your custom implementation, if your use case is drastically different.
- Use modules in other environments, such as Edge functions and Next.js apps.
---
## How to Extend Data Model of Another Module?
To extend the data model of another module, such as the `product` data model of the Product Module, use Medusa's linking concepts as explained in the [Module Links chapters](../../../fundamentals/module-links/page.mdx).
---
## How to Use Services of Other Modules?
If you're building a feature that uses functionalities from different modules, use a workflow whose steps resolve the modules' services to perform these functionalities.
Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output.
### Example
For example, consider you have two modules:
1. A module that stores and manages brands in your application.
2. A module that integrates a third-party Content Management System (CMS).
To sync brands from your application to the third-party system, create the following steps:
export const stepsHighlights = [
["1", "retrieveBrandsStep", "A step that retrieves brands using a brand module."],
["14", "createBrandsInCmsStep", "A step that creates brands using a CMS module."],
["25", "", "Add a compensation function to the step if an error occurs."]
]
```ts title="Example Steps" highlights={stepsHighlights}
const retrieveBrandsStep = createStep(
"retrieve-brands",
async (_, { container }) => {
const brandModuleService = container.resolve(
"brandModuleService"
)
const brands = await brandModuleService.listBrands()
return new StepResponse(brands)
}
)
const createBrandsInCmsStep = createStep(
"create-brands-in-cms",
async ({ brands }, { container }) => {
const cmsModuleService = container.resolve(
"cmsModuleService"
)
const cmsBrands = await cmsModuleService.createBrands(brands)
return new StepResponse(cmsBrands, cmsBrands)
},
async (brands, { container }) => {
const cmsModuleService = container.resolve(
"cmsModuleService"
)
await cmsModuleService.deleteBrands(
brands.map((brand) => brand.id)
)
}
)
```
The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module.
Then, create the following workflow that uses these steps:
```ts title="Example Workflow"
export const syncBrandsWorkflow = createWorkflow(
"sync-brands",
() => {
const brands = retrieveBrandsStep()
updateBrandsInCmsStep({ brands })
}
)
```
You can then use this workflow in an API route, scheduled job, or other resources that use this functionality.
@@ -0,0 +1,287 @@
import { Prerequisites } from "docs-ui"
export const metadata = {
title: `${pageNumber} Loaders`,
}
# {metadata.title}
In this chapter, youll learn about loaders and how to use them.
## What is a Loader?
When building a commerce application, you'll often need to execute an action the first time the application starts. For example, if your application needs to connect to databases other than Medusa's PostgreSQL database, you might need to establish a connection on application startup.
In Medusa, you can execute an action when the application starts using a loader. A loader is a function exported by a [module](../page.mdx), which is a package of business logic for a single domain. When the Medusa application starts, it executes all loaders exported by configured modules.
Loaders are useful to register custom resources, such as database connections, in the [module's container](../container/page.mdx), which is similar to the [Medusa container](../../medusa-container/page.mdx) but includes only [resources available to the module](!resources!/medusa-container-resources#module-container-resources). Modules are isolated, so they can't access resources outside of them, such as a service in another module.
<Note title="Why are modules isolated?">
Medusa isolates modules to ensure that they're re-usable across applications, aren't tightly coupled to other resources, and don't have implications when integrated into the Medusa application. Learn more about why modules are isolated in [this chapter](../isolation/page.mdx), and check out [this reference for the list of resources in the module's container](!resources!/medusa-container-resources#module-container-resources).
</Note>
---
## How to Create a Loader?
### 1. Implement Loader Function
You create a loader function in a TypeScript or JavaScript file under a module's `loaders` directory.
For example, consider you have a `hello` module, you can create a loader at `src/modules/hello/loaders/hello-world.ts` with the following content:
![Example of loader file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732865671/Medusa%20Book/loader-dir-overview_eg6vtu.jpg)
<Note title="Tip">
Learn how to create a module in [this chapter](../page.mdx).
</Note>
```ts title="src/modules/hello/loaders/hello-world.ts"
import {
LoaderOptions,
} from "@medusajs/framework/types"
export default async function helloWorldLoader({
container,
}: LoaderOptions) {
const logger = container.resolve("logger")
logger.info("[helloWorldLoader]: Hello, World!")
}
```
The loader file exports an async function, which is the function executed when the application loads.
The function receives an object parameter that has a `container` property, which is the module's container that you can use to resolve resources from. In this example, you resolve the Logger utility to log a message in the terminal.
<Note title="Tip">
Find the list of resources in the module's container in [this reference](!resources!/medusa-container-resources#module-container-resources).
</Note>
### 2. Export Loader in Module Definition
After implementing the loader, you must export it in the module's definition in the `index.ts` file at the root of the module's directory. Otherwise, the Medusa application will not run it.
So, to export the loader you implemented above in the `hello` module, add the following to `src/modules/hello/index.ts`:
```ts title="src/modules/hello/index.ts"
// other imports...
import helloWorldLoader from "./loaders/hello-world"
export default Module("hello", {
// ...
loaders: [helloWorldLoader],
})
```
The second parameter of the `Module` function accepts a `loaders` property whose value is an array of loader functions. The Medusa application will execute these functions when it starts.
### Test the Loader
Assuming your module is [added to Medusa's configuration](../page.mdx#4-add-module-to-medusas-configurations), you can test the loader by starting the Medusa application:
```bash npm2yarn
npm run dev
```
Then, you'll find the following message logged in the terminal:
```plain
info: [HELLO MODULE] Just started the Medusa application!
```
This indicates that the loader in the `hello` module ran and logged this message.
---
## Example: Register Custom MongoDB Connection
As mentioned in this chapter's introduction, loaders are most useful when you need to register a custom resource in the module's container to re-use it in other customizations in the module.
Consider your have a MongoDB module that allows you to perform operations on a MongoDB database.
<Prerequisites
items={[
{
text: "MongoDB database that you can connect to from a local machine.",
link: "https://www.mongodb.com"
},
{
text: "Install the MongoDB SDK in your Medusa application.",
link: "https://www.mongodb.com/docs/drivers/node/current/quick-start/download-and-install/#install-the-node.js-driver"
}
]}
/>
To connect to the database, you create the following loader in your module:
export const loaderHighlights = [
["5", "ModuleOptions", "Define a type for expected options."],
["13", "ModuleOptions", "Pass the option type as a type argument to `LoaderOptions`."],
["23", "clientDb", "Create a client instance that connects to the specified database."],
["29", "register", "Register custom resource in the container."],
["30", `"mongoClient"`, "The resource's key in the container."],
["31", "asValue(clientDb)", "The resource to register."]
]
```ts title="src/modules/mongo/loaders/connection.ts" highlights={loaderHighlights}
import { LoaderOptions } from "@medusajs/framework/types"
import { asValue } from "awilix"
import { MongoClient } from "mongodb"
type ModuleOptions = {
connection_url?: string
db_name?: string
}
export default async function mongoConnectionLoader({
container,
options,
}: LoaderOptions<ModuleOptions>) {
if (!options.connection_url) {
throw new Error(`[MONGO MDOULE]: connection_url option is required.`)
}
if (!options.db_name) {
throw new Error(`[MONGO MDOULE]: db_name option is required.`)
}
const logger = container.resolve("logger")
try {
const clientDb = (
await (new MongoClient(options.connection_url)).connect()
).db(options.db_name)
logger.info("Connected to MongoDB")
container.register(
"mongoClient",
asValue(clientDb)
)
} catch (e) {
logger.error(
`[MONGO MDOULE]: An error occurred while connecting to MongoDB: ${e}`
)
}
}
```
The loader function accepts in its object parameter an `options` property, which is the options passed to the module in Medusa's configurations. For example:
export const optionHighlights = [
["6", "options", "The options to pass to the module."]
]
```ts title="medusa-config.ts" highlights={optionHighlights}
module.exports = defineConfig({
// ...
modules: [
{
resolve: "./src/modules/mongo",
options: {
connection_url: process.env.MONGO_CONNECTION_URL,
db_name: process.env.MONGO_DB_NAME,
},
},
],
})
```
Passing options is useful when your module needs informations like connection URLs or API keys, as it ensures your module can be re-usable across applications. For the MongoDB Module, you expect two options:
- `connection_url`: the URL to connect to the MongoDB database.
- `db_name`: The name of the database to connect to.
In the loader, you check first that these options are set before proceeding. Then, you create an instance of the MongoDB client and connect to the database specified in the options.
After creating the client, you register it in the module's container using the container's `register` method. The method accepts two parameters:
1. The key to register the resource under, which in this case is `mongoClient`. You'll use this name later to resolve the client.
2. The resource to register in the container, which is the MongoDB client you created. However, you don't pass the resource as-is. Instead, you need to use an `asValue` function imported from the [awilix package](https://github.com/jeffijoe/awilix), which is the package used to implement the container functionality in Medusa.
### Use Custom Registered Resource in Module's Service
After registering the custom MongoDB client in the module's container, you can now resolve and use it in the module's service.
For example:
export const serviceHighlights = [
["10", "mongoClient", "Resolve the MongoDB client from the container."],
["11", "mongoClient_", "Set the MongoDB client as a class property."],
["14", "createMovie", "Add a method that uses the MongoDB client to create a document."],
["30", "deleteMovie", "Add a method that uses the MongoDB client to delete a document."]
]
```ts title="src/modules/mongo/service.ts"
import type { Db } from "mongodb"
type InjectedDependencies = {
mongoClient: Db
}
export default class MongoModuleService {
private mongoClient_: Db
constructor({ mongoClient }: InjectedDependencies) {
this.mongoClient_ = mongoClient
}
async createMovie({ title }: {
title: string
}) {
const moviesCol = this.mongoClient_.collection("movie")
const insertedMovie = await moviesCol.insertOne({
title,
})
const movie = await moviesCol.findOne({
_id: insertedMovie.insertedId,
})
return movie
}
async deleteMovie(id: string) {
const moviesCol = this.mongoClient_.collection("movie")
await moviesCol.deleteOne({
_id: {
equals: id,
},
})
}
}
```
The service `MongoModuleService` resolves the `mongoClient` resource you registered in the loader and sets it as a class property. You then use it in the `createMovie` and `deleteMovie` methods, which create and delete a document in a `movie` collection in the MongoDB database, respectively.
Make sure to export the loader in the module's definition in the `index.ts` file at the root directory of the module:
```ts title="src/modules/mongo/index.ts" highlights={[["9"]]}
import { Module } from "@medusajs/framework/utils"
import MongoModuleService from "./service"
import mongoConnectionLoader from "./loaders/connection"
export const MONGO_MODULE = "mongo"
export default Module(MONGO_MODULE, {
service: MongoModuleService,
loaders: [mongoConnectionLoader],
})
```
### Test it Out
You can test the connection out by starting the Medusa application. If it's successful, you'll see the following message logged in the terminal:
```bash
info: Connected to MongoDB
```
You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database.
@@ -0,0 +1,29 @@
export const metadata = {
title: `${pageNumber} Modules Directory Structure`,
}
# {metadata.title}
In this document, you'll learn about the expected files and directories in your module.
![Module Directory Structure Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1714379976/Medusa%20Book/modules-dir-overview_nqq7ne.jpg)
## index.ts
The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](../page.mdx).
---
## service.ts
A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](../page.mdx).
---
## Other Directories
The following directories are optional and their content are explained more in the following chapters:
- `models`: Holds the data models representing tables in the database.
- `migrations`: Holds the migration files used to reflect changes on the database.
- `loaders`: Holds the scripts to run on the Medusa application's start-up.
@@ -0,0 +1,130 @@
export const metadata = {
title: `${pageNumber} Multiple Services in a Module`,
}
# {metadata.title}
In this chapter, you'll learn how to use multiple services in a module.
## Module's Main and Internal Services
A module has one main service only, which is the service exported in the module's definition.
However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources.
---
## How to Add an Internal Service
### 1. Create Service
To add an internal service, create it in the `services` directory of your module.
For example, create the file `src/modules/hello/services/client.ts` with the following content:
```ts title="src/modules/hello/services/client.ts"
export class ClientService {
async getMessage(): Promise<string> {
return "Hello, World!"
}
}
```
### 2. Export Service in Index
Next, create an `index.ts` file under the `services` directory of the module that exports your internal services.
For example, create the file `src/modules/hello/services/index.ts` with the following content:
```ts title="src/modules/hello/services/index.ts"
export * from "./client"
```
This exports the `ClientService`.
### 3. Resolve Internal Service
Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders.
For example, in your main service:
```ts title="src/modules/hello/service.ts" highlights={[["5"], ["13"]]}
// other imports...
import { ClientService } from "./services"
type InjectedDependencies = {
clientService: ClientService
}
class HelloModuleService extends MedusaService({
MyCustom,
}){
protected clientService_: ClientService
constructor({ clientService }: InjectedDependencies) {
super(...arguments)
this.clientService_ = clientService
}
}
```
You can now use your internal service in your main service.
---
## Resolve Resources in Internal Service
Resolve dependencies from your module's container in the constructor of your internal service.
For example:
```ts
import { Logger } from "@medusajs/framework/types"
type InjectedDependencies = {
logger: Logger
}
export class ClientService {
protected logger_: Logger
constructor({ logger }: InjectedDependencies) {
this.logger_ = logger
}
}
```
---
## Access Module Options
Your internal service can't access the module's options.
To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`.
For example:
```ts
import { ConfigModule } from "@medusajs/framework/types"
import { HELLO_MODULE } from ".."
export type InjectedDependencies = {
configModule: ConfigModule
}
export class ClientService {
protected options: Record<string, any>
constructor({ configModule }: InjectedDependencies) {
const moduleDef = configModule.modules[HELLO_MODULE]
if (typeof moduleDef !== "boolean") {
this.options = moduleDef.options
}
}
}
```
The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key.
If its value is not a `boolean`, set the service's options to the module configuration's `options` property.
@@ -0,0 +1,100 @@
export const metadata = {
title: `${pageNumber} Module Options`,
}
# {metadata.title}
In this chapter, youll learn about passing options to your module from the Medusa applications configurations and using them in the modules resources.
## What are Module Options?
A module can receive options to customize or configure its functionality.
For example, if youre creating a module that integrates a third-party service, youll want to receive the integration credentials in the options rather than adding them directly in your code.
---
## How to Pass Options to a Module?
To pass options to a module, add an `options` property to the modules configuration in `medusa-config.ts`.
For example:
```js title="medusa-config.ts"
module.exports = defineConfig({
// ...
modules: [
{
resolve: "./src/modules/hello",
options: {
capitalize: true,
},
},
],
})
```
The `options` propertys value is an object. You can pass any properties you want.
---
## Access Module Options in Main Service
The modules main service receives the module options as a second parameter.
For example:
```ts title="src/modules/hello/service.ts" highlights={[["12"], ["14", "options?: ModuleOptions"], ["17"], ["18"], ["19"]]}
import { MedusaService } from "@medusajs/framework/utils"
import MyCustom from "./models/my-custom"
// recommended to define type in another file
type ModuleOptions = {
capitalize?: boolean
}
export default class HelloModuleService extends MedusaService({
MyCustom,
}){
protected options_: ModuleOptions
constructor({}, options?: ModuleOptions) {
super(...arguments)
this.options_ = options || {
capitalize: false,
}
}
// ...
}
```
---
## Access Module Options in Loader
The object that a modules loaders receive as a parameter has an `options` property holding the module's options.
For example:
```ts title="src/modules/hello/loaders/hello-world.ts" highlights={[["11"], ["12", "ModuleOptions", "The type of expected module options."], ["16"]]}
import {
LoaderOptions,
} from "@medusajs/framework/types"
// recommended to define type in another file
type ModuleOptions = {
capitalize?: boolean
}
export default async function helloWorldLoader({
options,
}: LoaderOptions<ModuleOptions>) {
console.log(
"[HELLO MODULE] Just started the Medusa application!",
options
)
}
```
@@ -0,0 +1,329 @@
export const metadata = {
title: `${pageNumber} Modules`,
}
# {metadata.title}
In this chapter, youll learn about modules and how to create them.
## What is a Module?
A module is a reusable package of functionalities related to a single domain or integration. Medusa comes with multiple pre-built modules for core commerce needs, such as the [Cart Module](!resources!/commerce-modules/cart) that holds the data models and business logic for cart operations.
When building a commerce application, you often need to introduce custom behavior specific to your products, tech stack, or your general ways of working. In other commerce platforms, introducing custom business logic and data models requires setting up separate applications to manage these customizations.
Medusa removes this overhead by allowing you to easily write custom modules that integrate into the Medusa application without implications on the existing setup. You can also re-use your modules across Medusa projects.
As you learn more about Medusa, you will see that modules are central to customizations and integrations. With modules, your Medusa application can turn into a middleware solution for your commerce ecosystem.
---
## How to Create a Module?
In a module, you define data models that represent new tables in the database, and you manage these models in a class called a service. Then, the Medusa application registers the module's service in the [Medusa container](../../fundamentals/medusa-container/page.mdx) so that you can build commerce flows and features around the functionalities provided by the module.
In this section, you'll build a Blog Module that has a `Post` data model and a service to manage that data model, you'll expose an API endpoint to create a blog post.
Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/blog`.
### 1. Create Data Model
A data model represents a table in the database. You create data models using Medusa's data modeling utility. It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.
You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a `Post` data model in the Blog Module, create the file `src/modules/blog/models/post.ts` with the following content:
![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg)
```ts title="src/modules/blog/models/post.ts"
import { model } from "@medusajs/framework/utils"
const Post = model.define("post", {
id: model.id().primaryKey(),
title: model.text(),
})
export default Post
```
You define the data model using the `define` method of the `model` utility imported from `@medusajs/framework/utils`. It accepts two parameters:
1. The first one is the name of the data model's table in the database. Use snake-case names.
2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods, such as `text` and `id`.
- Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`, so you don't need to add them manually.
<Note title="Tip">
Learn about other property types in [this chapter](../../fundamentals/data-models/property-types/page.mdx).
</Note>
The code snippet above defines a `Post` data model with `id` and `title` properties.
### 2. Create Service
You perform database operations on your data models in a service, which is a class exported by the module and acts like an interface to its functionalities. Medusa registers the service in its [container](../../fundamentals/medusa-container/page.mdx), allowing you to resolve and use it when building custom commerce flows.
You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, to create the Blog Module's service, create the file `src/modules/blog/service.ts` with the following content:
![Updated directory overview after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732807230/Medusa%20Book/blog-dir-overview-2_avzb9l.jpg)
export const highlights = [
["4", "MedusaService", "The service factory function."],
["5", "MyCustom", "The data models to generate data-management methods for."]
]
```ts title="src/modules/blog/service.ts" highlights={highlights}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
class BlogModuleService extends MedusaService({
Post,
}){
}
export default BlogModuleService
```
Your module's service extends a class generated by the `MedusaService` utility function. This class comes with generated methods for data-management Create, Read, Update, and Delete (CRUD) operations on each of your modules, saving your time that can be spent on building custom business logic.
The `MedusaService` function accepts an object of data models to generate methods for. You can pass all data models in your module in this object.
For example, the `BlogModuleService` now has a `createPosts` method to create post records, and a `retrievePost` method to retrieve a post record. The suffix of each method (except for `retrieve`) is the pluralized name of the data model.
<Note>
Find all methods generated by the `MedusaService` in [this reference](!resources!/service-factory-reference)
</Note>
If a module doesn't have data models, such as when it's integrating a third-party service, it doesn't need to extend `MedusaService`.
### 3. Export Module Definition
The final piece to a module is its definition, which is exported in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its main service. Medusa will then register the main service in the container under the module's name.
So, to export the definition of the Blog Module, create the file `src/modules/blog/index.ts` with the following content:
![Updated directory overview after adding the module definition](https://res.cloudinary.com/dza7lstvk/image/upload/v1732808511/Medusa%20Book/blog-dir-overview-3_dcgjaa.jpg)
export const moduleDefinitionHighlights = [
["4", "BLOG_MODULE", "Export the module's name to reference it in other customizations."],
["6", "BLOG_MODULE", "Specify the module's name."],
["7", "service", "Specify the module's main service."]
]
```ts title="src/modules/blog/index.ts" highlights={moduleDefinitionHighlights}
import BlogModuleService from "./service"
import { Module } from "@medusajs/framework/utils"
export const BLOG_MODULE = "blog"
export default Module(BLOG_MODULE, {
service: BlogModuleService,
})
```
You use the `Module` function imported from `@medusajs/framework/utils` to create the module's definition. It accepts two parameters:
1. The name that the module's main service is registered under (`blog`).
2. An object with a required property `service` indicating the module's main service.
<Note title="Tip">
You export `BLOG_MODULE` to reference the module's name more reliably when resolving its service in other customizations.
</Note>
### 4. Add Module to Medusa's Configurations
Once you finish building the module, add it to Medusa's configurations to start using it. Medusa will then register the module's main service in the Medusa container, allowing you to resolve and use it in other customizations.
In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:
```ts title="medusa-config.ts" highlights={[["7"]]}
module.exports = defineConfig({
projectConfig: {
// ...
},
modules: [
{
resolve: "./src/modules/blog",
},
],
})
```
Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` packages name.
### 5. Generate Migrations
Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.
Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations.
You don't have to write migrations yourself. Medusa's CLI tool has a command that generates the migrations for you. You can also use this command again when you make changes to the module at a later point, and it will generate new migrations for that change.
To generate a migration for the Blog Module, run the following command in your Medusa application's directory:
```bash
npx medusa db:generate blog
```
The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following:
```ts
import { Migration } from "@mikro-orm/migrations"
export class Migration20241121103722 extends Migration {
async up(): Promise<void> {
this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));")
}
async down(): Promise<void> {
this.addSql("drop table if exists \"post\" cascade;")
}
}
```
In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table.
### 6. Run Migrations
To reflect the changes in the generated migration file on the database, run the `db:migrate` command:
```bash
npx medusa db:migrate
```
This creates the `post` table in the database.
---
## Test the Module
Since the module's main service is registered in the Medusa container, you can resolve it in other customizations to use its methods.
To test out the Blog Module, you'll add the functionality to create a post in a [workflow](../../fundamentals/workflows/page.mdx), which is a special function that performs a task in a series of steps with rollback logic. Then, you'll expose an [API route](../api-routes/page.mdx) that creates a blog post by executing the workflow.
<Note title="Why use a workflow?">
By building a commerce feature in a workflow, you can execute it in other customizations while ensuring data consistency across systems. If an error occurs during execution, every step has its own rollback logic to undo its actions. Workflows have other special features which you can learn about in [this chapter](../../fundamentals/workflows/page.mdx).
</Note>
To create the workflow, create the file `src/workflows/create-post.ts` with the following content:
export const workflowHighlights = [
["14", "createPostStep", "Define a step that creates a post using the Blog Module."],
["17", "resolve", "Resolve the Blog Module's service from the Medusa container."],
["19", "createPosts", "Create a blog post using the Blog Module service's generated method."],
["25", "", "Add a compensation function that only runs if an error occurs in the workflow."],
["28", "", "Delete the post if an error occurs using the Blog Module service's generated method."],
["32", "createWorkflow", "Create and workflow that can be executed to create a blog post."],
["35", "createPostStep", "Execute the `createPostStep` to create the post."]
]
```ts title="src/workflows/create-post.ts" highlights={workflowHighlights}
import {
createStep,
createWorkflow,
StepResponse,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { BLOG_MODULE } from "../modules/blog"
import BlogModuleService from "../modules/blog/service"
type CreatePostWorkflowInput = {
title: string
}
const createPostStep = createStep(
"create-post",
async ({ title }: CreatePostWorkflowInput, { container }) => {
const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE)
const post = await blogModuleService.createPosts({
title,
})
return new StepResponse(post, post)
},
async (post, { container }) => {
const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE)
await blogModuleService.deletePosts(post.id)
}
)
export const createPostWorkflow = createWorkflow(
"create-post",
(postInput: CreatePostWorkflowInput) => {
const post = createPostStep(postInput)
return new WorkflowResponse(post)
}
)
```
The workflow has a single step `createPostStep` that creates a post. In the step, you resolve the Blog Module's service from the Medusa container, which the step receives as a parameter. Then, you create the post using the method `createPosts` of the service, which was generated by `MedusaService`.
The step also has a compensation function, which is a function passed as a third-parameter to `createStep` that implements the logic to rollback the change made by a step in case an error occurs during the workflow's execution.
You'll now execute that workflow in an API route to expose the feature of creating blog posts to clients. To create an API route, create the file `src/api/blog/posts/route.ts` with the following content:
```ts
import type {
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import {
createPostWorkflow,
} from "../../../workflows/create-post"
export async function POST(
req: MedusaRequest,
res: MedusaResponse
) {
const { result: post } = await createPostWorkflow(req.scope)
.run({
input: {
title: "My Post",
},
})
res.json({
post,
})
}
```
This adds a `POST` API route at `/blog/posts`. In the API route, you execute the `createPostWorkflow` by invoking it, passing it the Medusa container in `req.scope`, then invoking the `run` method. In the `run` method, you pass the workflow's input in the `input` property.
To test this out, start the Medusa application:
```bash npm2yarn
npm run dev
```
Then, send a `POST` request to `/blog/posts`:
```bash
curl -X POST http://localhost:9000/blog/posts
```
This will create a post and return it in the response:
```json
{
"post": {
"id": "123...",
"title": "My Post",
"created_at": "...",
"updated_at": "..."
}
}
```
You can also execute the workflow from a [subscriber](../../fundamentals/events-and-subscribers/page.mdx) when an event occurs, or from a [scheduled job](../../fundamentals/scheduled-jobs/page.mdx) to run it at a specified interval.
@@ -0,0 +1,40 @@
export const metadata = {
title: `${pageNumber} Service Constraints`,
}
# {metadata.title}
This chapter lists constraints to keep in mind when creating a service.
## Use Async Methods
Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronous, it always executes methods in the wrapper with the `await` keyword.
For example, if you have a synchronous `getMessage` method, and you use it in other resources like workflows, Medusa executes it as an async method:
```ts
await helloModuleService.getMessage()
```
So, make sure your service's methods are always async to avoid unexpected errors or behavior.
```ts highlights={[["8", "", "Method must be async."], ["13", "async", "Correct way of defining the method."]]}
import { MedusaService } from "@medusajs/framework/utils"
import MyCustom from "./models/my-custom"
class HelloModuleService extends MedusaService({
MyCustom,
}){
// Don't
getMessage(): string {
return "Hello, World!"
}
// Do
async getMessage(): Promise<string> {
return "Hello, World!"
}
}
export default HelloModuleService
```
@@ -0,0 +1,299 @@
import { Tabs, TabsContent, TabsContentWrapper, TabsList, TabsTriggerVertical } from "docs-ui"
export const metadata = {
title: `${pageNumber} Service Factory`,
}
# {metadata.title}
In this chapter, youll learn about what the service factory is and how to use it.
## What is the Service Factory?
Medusa provides a service factory that your modules main service can extend.
The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually.
<Note title="Extend the service factory when" type="success">
Your service provides data-management functionalities of your data models.
</Note>
---
## How to Extend the Service Factory?
Medusa provides the service factory as a `MedusaService` function your service extends. The function creates and returns a service class with generated data-management methods.
For example, create the file `src/modules/hello/service.ts` with the following content:
export const highlights = [
["4", "MedusaService", "The service factory function."],
["5", "MyCustom", "The data models to generate data-management methods for."]
]
```ts title="src/modules/hello/service.ts" highlights={highlights}
import { MedusaService } from "@medusajs/framework/utils"
import MyCustom from "./models/my-custom"
class HelloModuleService extends MedusaService({
MyCustom,
}){
// TODO implement custom methods
}
export default HelloModuleService
```
### MedusaService Parameters
The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for.
In the example above, since the `HelloModuleService` extends `MedusaService`, it has methods to manage the `MyCustom` data model, such as `createMyCustoms`.
### Generated Methods
The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database.
The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`.
For example, the following methods are generated for the service above:
<Note>
Find a complete reference of each of the methods in [this documentation](!resources!/service-factory-reference)
</Note>
<Tabs defaultValue="listMyCustoms" layoutType="vertical" className="mt-2">
<TabsList>
<TabsTriggerVertical value="listMyCustoms">listMyCustoms</TabsTriggerVertical>
<TabsTriggerVertical value="listAndCountMyCustoms">listAndCount</TabsTriggerVertical>
<TabsTriggerVertical value="retrieveMyCustom">retrieveMyCustom</TabsTriggerVertical>
<TabsTriggerVertical value="createMyCustoms">createMyCustoms</TabsTriggerVertical>
<TabsTriggerVertical value="updateMyCustoms">updateMyCustoms</TabsTriggerVertical>
<TabsTriggerVertical value="deleteMyCustoms">deleteMyCustoms</TabsTriggerVertical>
<TabsTriggerVertical value="softDeleteMyCustoms">softDeleteMyCustoms</TabsTriggerVertical>
<TabsTriggerVertical value="restoreMyCustoms">restoreMyCustoms</TabsTriggerVertical>
</TabsList>
<TabsContentWrapper className="[&_h3]:!mt-0">
<TabsContent value="listMyCustoms">
### listMyCustoms
This method retrieves an array of records based on filters and pagination configurations.
For example:
```ts
const myCustoms = await helloModuleService
.listMyCustoms()
// with filters
const myCustoms = await helloModuleService
.listMyCustoms({
id: ["123"]
})
```
</TabsContent>
<TabsContent value="listAndCountMyCustoms">
### listAndCountMyCustoms
This method retrieves a tuple of an array of records and the total count of available records based on the filters and pagination configurations provided.
For example:
```ts
const [
myCustoms,
count
] = await helloModuleService.listAndCountMyCustoms()
// with filters
const [
myCustoms,
count
] = await helloModuleService.listAndCountMyCustoms({
id: ["123"]
})
```
</TabsContent>
<TabsContent value="retrieveMyCustom">
### retrieveMyCustom
This method retrieves a record by its ID.
For example:
```ts
const myCustom = await helloModuleService
.retrieveMyCustom("123")
```
</TabsContent>
<TabsContent value="createMyCustoms">
### createMyCustoms
This method creates and retrieves records of the data model.
For example:
```ts
const myCustom = await helloModuleService
.createMyCustoms({
name: "test"
})
// create multiple
const myCustoms = await helloModuleService
.createMyCustoms([
{
name: "test"
},
{
name: "test 2"
},
])
```
</TabsContent>
<TabsContent value="updateMyCustoms">
### updateMyCustoms
This method updates and retrieves records of the data model.
For example:
```ts
const myCustom = await helloModuleService
.updateMyCustoms({
id: "123",
name: "test"
})
// update multiple
const myCustoms = await helloModuleService
.updateMyCustoms([
{
id: "123",
name: "test"
},
{
id: "321",
name: "test 2"
},
])
// use filters
const myCustoms = await helloModuleService
.updateMyCustoms([
{
selector: {
id: ["123", "321"]
},
data: {
name: "test"
}
},
])
```
</TabsContent>
<TabsContent value="deleteMyCustoms">
### deleteMyCustoms
This method deletes records by an ID or filter.
For example:
```ts
await helloModuleService.deleteMyCustoms("123")
// delete multiple
await helloModuleService.deleteMyCustoms([
"123", "321"
])
// use filters
await helloModuleService.deleteMyCustoms({
selector: {
id: ["123", "321"]
}
})
```
</TabsContent>
<TabsContent value="softDeleteMyCustoms">
### softDeleteMyCustoms
This method soft-deletes records using an array of IDs or an object of filters.
For example:
```ts
await helloModuleService.softDeleteMyCustoms("123")
// soft-delete multiple
await helloModuleService.softDeleteMyCustoms([
"123", "321"
])
// use filters
await helloModuleService.softDeleteMyCustoms({
id: ["123", "321"]
})
```
</TabsContent>
<TabsContent value="restoreMyCustoms">
### restoreMyCustoms
This method restores soft-deleted records using an array of IDs or an object of filters.
For example:
```ts
await helloModuleService.restoreMyCustoms([
"123", "321"
])
// use filters
await helloModuleService.restoreMyCustoms({
id: ["123", "321"]
})
```
</TabsContent>
</TabsContentWrapper>
</Tabs>
### Using a Constructor
If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`.
For example:
```ts highlights={[["8"]]}
import { MedusaService } from "@medusajs/framework/utils"
import MyCustom from "./models/my-custom"
class HelloModuleService extends MedusaService({
MyCustom,
}){
constructor() {
super(...arguments)
}
}
export default HelloModuleService
```