docs: improvements + additions to module docs (#9152)

- Split Module and Module Links to their own chapters
- Add new docs on db operations and transactions in modules, multiple services, links with custom columns, etc...
- Added a list of registered dependencies in a module container
This commit is contained in:
Shahed Nasser
2024-10-01 14:20:54 +03:00
committed by GitHub
parent 1ad7e7583f
commit fb67d90b64
32 changed files with 1138 additions and 102 deletions
@@ -8,11 +8,13 @@ In this chapter, you'll learn about the module's container and how to resolve re
## Module's Container
Each module has a local container only used by the resources of that module.
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, such as:
So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container.
- `logger`: A utility to log message in the Medusa application's logs.
### List of Registered Resources
Find a list of resources or dependencies registered in a module's container in [this Learning Resources reference](!resoures!/medusa-container-resources).
---
@@ -0,0 +1,464 @@
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 = [
["4", "getCount", "Retrieves the number of records in `my_custom` using the `count` method."],
["8", "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"
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 uncomitted 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 connect 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 method.
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
}
)
}
}
```
@@ -8,8 +8,8 @@ In this chapter, you'll learn how modules are isolated, and what that means for
<Note title="Summary">
- Modules can't access resources, such as services, from other modules.
- You can use Medusa's tools, as explained in the next chapters, to extend a modules' features or implement features across modules.
- 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](../../module-links/page.mdx), to extend a module's data models and retrieve data across modules.
</Note>
@@ -21,12 +21,93 @@ For example, your custom module can't resolve the Product Module's main service
---
## How to Implement Custom Features Across Modules?
## Why are Modules Isolated
In your Medusa application, you want to implement features that span across modules, or you want to extend an existing module's features and customize them for your own use case.
Some of the module isolation's benefits include:
For example, you want to extend the Product Module to add new properties to the `Product` data model.
- 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.
Medusa provides the tools to implement these use cases while maintaining isolation between modules.
---
The next chapters explain these tools and how to use them in your custom development.
## 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](../../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.
@@ -1,61 +0,0 @@
export const metadata = {
title: `${pageNumber} Module Link Direction`,
}
# {metadata.title}
In this chapter, you'll learn about difference in module link directions, and which to use based on your use case.
## Link Direction
The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`.
For example, the following defines a link from the `helloModuleService`'s `myCustom` data model to the Product Module's `product` data model:
```ts
export default defineLink(
HelloModule.linkable.myCustom,
ProductModule.linkable.product
)
```
Whereas the following defines a link from the Product Module's `product` data model to the `helloModuleService`'s `myCustom` data model:
```ts
export default defineLink(
ProductModule.linkable.product,
HelloModule.linkable.myCustom
)
```
The above links are two different links that serve different purposes.
---
## Which Link Direction to Use?
### Extend Data Models
If you're adding a link to a data model to extend it and add new fields, define the link from the main data model to the custom data model.
For example, if the `myCustom` data model adds new fields to the `product` data model, define the link from `product` to `myCustom`:
```ts
export default defineLink(
ProductModule.linkable.product,
HelloModule.linkable.myCustom
)
```
### Associate Data Models
If you're linking data models to indicate an association between them, define the link from the custom data model to the main data model.
For example, if the `myCustom` data model is associated to the `product` data model, define the link from `myCustom` to `product`:
```ts
export default defineLink(
HelloModule.linkable.myCustom,
ProductModule.linkable.product
)
```
@@ -1,145 +0,0 @@
import { BetaBadge } from "docs-ui"
export const metadata = {
title: `${pageNumber} Module Link`,
}
# {metadata.title} <BetaBadge text="Beta" tooltipText="Module links are in active development." />
In this chapter, youll learn what a module link is.
## What is a Module Link?
A module link forms an association between two data models of different modules, while maintaining module isolation.
You can then retrieve data across the linked modules, and manage their linked records.
<Note title="Use module links when" type="success">
You want to create a relation between data models from different modules.
</Note>
<Note title="Don't use module links if" type="error">
You want to create a relationship between data models in the same module. Use data model relationships instead.
</Note>
---
## How to Define a Module Link?
### 1. Create Link File
Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines the link using the `defineLink` function imported from `@medusajs/framework/utils` and exports it.
For example:
export const highlights = [
["6", "linkable", "Special `linkable` property that holds the linkable data models of `HelloModule`."],
["7", "linkable", "Special `linkable` property that holds the linkable data models of `ProductModule`."],
]
```ts title="src/links/hello-product.ts" highlights={highlights}
import HelloModule from "../modules/hello"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"
export default defineLink(
ProductModule.linkable.product,
HelloModule.linkable.myCustom
)
```
The `defineLink` function accepts as parameters the link configurations of each module's data model. A module has a special `linkable` property that holds these configurations for its data models.
In this example, you define a module link between the `hello` module's `MyCustom` data model and the Product Module's `Product` data model.
### 2. Sync Links
Medusa stores links as pivot tables in the database. So, to reflect your link in the database, run the `db:sync-links` command:
```bash
npx medusa db:sync-links
```
Use this command whenever you make changes to your links. For example, run this command if you remove your link definition file.
<Note title="Tip">
You can also use the `db:migrate` command, which both runs the migrations and syncs the links.
</Note>
---
## Define a List Link
By default, the defined link establishes a one-to-one relation: a record of a data model is linked to one record of the other data model.
To specify that a data model can have multiple of its records linked to the other data model's record, use the `isList` option.
For example:
```ts
import HelloModule from "../modules/hello"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"
export default defineLink(
ProductModule.linkable.product,
{
linkable: HelloModule.linkable.myCustom,
isList: true,
}
)
```
In this case, you pass an object of configuration as a parameter instead. The object accepts the following properties:
- `linkable`: The data model's link configuration.
- `isList`: Whether multiple records can be linked to one record of the other data model.
In this example, a record of `product` can be linked to more than one record of `myCustom`.
---
## Extend Data Models with Module Links
Module links are most useful when you want to add properties to a data model of another module.
For example, to add custom properties to the `Product` data model of the Product Module, you:
1. Create a module.
2. Create in the module a data model that holds the custom properties you want to add to the `Product` data model.
2. Define a module link that links your module to the Product Module.
Then, in the next chapters, you'll learn how to:
- Link each product to a record of your data model.
- Retrieve your data model's properties when you retrieve products.
---
## Set Delete Cascades on Link
To enable delete cascade on a link so that when a record is deleted, its linked records are also deleted, pass the `deleteCascades` property in the object passed to `defineLink`.
For example:
```ts
import HelloModule from "../modules/hello"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"
export default defineLink(
ProductModule.linkable.product,
{
linkable: HelloModule.linkable.myCustom,
deleteCascades: true,
}
)
```
In this example, when a product is deleted, its linked `myCustom` record is also deleted.
@@ -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.js`.
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.
@@ -1,5 +1,3 @@
import { CodeTabs, CodeTab } from "docs-ui"
export const metadata = {
title: `${pageNumber} Module Options`,
}
@@ -0,0 +1,15 @@
export const metadata = {
title: `${pageNumber} Modules Advanced Guides`,
}
# {metadata.title}
In the next chapters, you'll learn more about developing modules and related resources.
By the end of this chapter, you'll know more about:
1. A module's container and how a module is isolated.
2. Passing options to a module.
3. The service factory and the methods it generates.
4. Using a module's service to query and perform actions on the database.
5. Using multiple services in a module.
@@ -1,229 +0,0 @@
import { TypeList, Tabs, TabsList, TabsTriggerVertical, TabsContent, TabsContentWrapper } from "docs-ui"
export const metadata = {
title: `${pageNumber} Query`,
}
# {metadata.title}
In this chapter, youll learn about the Query utility and how to use it to fetch data from modules.
<Note type="soon" title="In Development">
Query is in development and is subject to change in future releases.
</Note>
## What is Query?
Query fetches data across modules. Its a set of methods registered in the Medusa container under the `query` key.
In your resources, such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusas commerce modules.
---
## Query Example
For example, create the route `src/api/query/route.ts` with the following content:
export const exampleHighlights = [
["13", "", "Resolve Query from the Medusa container."],
["15", "graph", "Run a query to retrieve data."],
["16", "entity", "The name of the data model you're querying."],
["17", "fields", "An array of the data models properties to retrieve in the result."],
]
```ts title="src/api/query/route.ts" highlights={exampleHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
import {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import {
ContainerRegistrationKeys,
} from "@medusajs/framework/utils"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
const { data: myCustoms } = await query.graph({
entity: "my_custom",
fields: ["id", "name"],
})
res.json({ my_customs: myCustoms })
}
```
In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key.
Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties:
- `entity`: The data model's name, as specified in the first parameter of the `model.define` method used for the data model's definition.
- `fields`: An array of the data models properties to retrieve in the result.
The method returns an object that has a `data` property, which holds an array of the retrieved data. For example:
```json title="Returned Data"
{
"data": [
{
"id": "123",
"name": "test"
}
]
}
```
---
## Querying the Graph
When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates.
This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them.
---
## Retrieve Linked Records
Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`.
For example:
```ts highlights={[["6"]]}
const { data: myCustoms } = await query.graph({
entity: "my_custom",
fields: [
"id",
"name",
"product.*",
],
})
```
<Note title="Tip">
`.*` means that all of data model's properties should be retrieved. To retrieve a specific property, replace the `*` with the property's name. For example, `product.title`.
</Note>
### Retrieve List Link Records
If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`.
For example:
```ts highlights={[["6"]]}
const { data: myCustoms } = await query.graph({
entity: "my_custom",
fields: [
"id",
"name",
"products.*",
],
})
```
---
## Apply Filters
```ts highlights={[["6"], ["7"], ["8"], ["9"]]}
const { data: myCustoms } = await query.graph({
entity: "my_custom",
fields: ["id", "name"],
filters: {
id: [
"mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
"mc_01HWSVWK3KYHKQEE6QGS2JC3FX",
],
},
})
```
The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records.
In the example above, you filter the `my_custom` records by multiple IDs.
<Note>
Filters don't apply on fields of linked data models from other modules.
</Note>
---
## Sort Records
```ts highlights={[["5"], ["6"], ["7"]]}
const { data: myCustoms } = await query.graph({
entity: "my_custom",
fields: ["id", "name"],
pagination: {
order: {
name: "DESC",
},
},
})
```
<Note>
Sorting doesn't work on fields of linked data models from other modules.
</Note>
The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records.
To sort returned records, pass an `order` property to `pagination`.
The `order` property is an object whose keys are property names, and values are either:
- `ASC` to sort records by that property in ascending order.
- `DESC` to sort records by that property in descending order.
---
## Apply Pagination
```ts highlights={[["8", "skip", "The number of records to skip before fetching the results."], ["9", "take", "The number of records to fetch."]]}
const {
data: myCustoms,
metadata: { count, take, skip },
} = await query.graph({
entity: "my_custom",
fields: ["id", "name"],
pagination: {
skip: 0,
take: 10,
},
})
```
To paginate the returned records, pass the following properties to `pagination`:
- `skip`: (required to apply pagination) The number of records to skip before fetching the results.
- `take`: The number of records to fetch.
When you provide the pagination fields, the `query.graph` method's returned object has a `metadata` property. Its value is an object having the following properties:
<TypeList types={[
{
name: "skip",
type: "`number`",
description: "The number of records skipped."
},
{
name: "take",
type: "`number`",
description: "The number of records requested to fetch."
},
{
name: "count",
type: "`number`",
description: "The total number of records."
}
]} sectionTitle="Apply Pagination" />
@@ -1,153 +0,0 @@
import { BetaBadge } from "docs-ui"
export const metadata = {
title: `${pageNumber} Remote Link`,
}
# {metadata.title} <BetaBadge text="Beta" tooltipText="Remote Links are in active development." />
In this chapter, youll learn what the remote link is and how to use it to manage links.
## What is the Remote Link?
The remote link is a class with utility methods to manage links between data models. Its registered in the Medusa container under the `remoteLink` registration name.
For example:
```ts collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
MedusaRequest,
MedusaResponse,
} from "@medusajs/medusa"
import {
ContainerRegistrationKeys,
} from "@medusajs/framework/utils"
import {
RemoteLink,
} from "@medusajs/framework/modules-sdk"
export async function POST(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const remoteLink: RemoteLink = req.scope.resolve(
ContainerRegistrationKeys.REMOTE_LINK
)
// ...
}
```
You can use its methods to manage links, such as create or delete links.
---
## Create Link
To create a link between records of two data models, use the `create` method of the remote link.
For example:
```ts
import { Modules } from "@medusajs/framework/utils"
// ...
await remoteLink.create({
[Modules.PRODUCT]: {
product_id: "prod_123",
},
"helloModuleService": {
my_custom_id: "mc_123",
},
})
```
The `create` method accepts as a parameter an object. The objects keys are the names of the linked modules.
<Note title="Important">
The keys (names of linked modules) must be in the same direction of the link definition.
</Note>
The value of each modules property is an object, whose keys are of the format `{data_model_snake_name}_id`, and values are the IDs of the linked record.
So, in the example above, you link a record of the `MyCustom` data model in a `hello` module to a `Product` record in the Product Module.
---
## Dismiss Link
To remove a link between records of two data models, use the `dismiss` method of the remote link.
For example:
```ts
import { Modules } from "@medusajs/framework/utils"
// ...
await remoteLink.dismiss({
[Modules.PRODUCT]: {
product_id: "prod_123",
},
"helloModuleService": {
my_custom_id: "mc_123",
},
})
```
The `dismiss` method accepts the same parameter type as the [create method](#create-link).
<Note title="Important">
The keys (names of linked modules) must be in the same direction of the link definition.
</Note>
---
## Cascade Delete Linked Records
If a record is deleted, use the `delete` method of the remote link to delete all linked records.
For example:
```ts
import { Modules } from "@medusajs/framework/utils"
// ...
await productModuleService.deleteVariants([variant.id])
await remoteLink.delete({
[Modules.PRODUCT]: {
product_id: "prod_123",
},
})
```
This deletes all records linked to the deleted product.
---
## Restore Linked Records
If a record that was previously soft-deleted is now restored, use the `restore` method of the remote link to restore all linked records.
For example:
```ts
import { Modules } from "@medusajs/framework/utils"
// ...
await productModuleService.restoreProducts(["prod_123"])
await remoteLink.restore({
[Modules.PRODUCT]: {
product_id: "prod_123",
},
})
```
@@ -8,7 +8,13 @@ This chapter lists constraints to keep in mind when creating a service.
## Use Async Methods
Medusa wraps adds wrappers around your service's methods and executes them as async methods.
Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronus, it always executes methods in the wrapper with the `await` keyword.
For example, if you have a synchronous `getMessage` method, and you use it 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.
@@ -12,7 +12,7 @@ In this chapter, youll learn about what the service factory is and how to use
Medusa provides a service factory that your modules main service can extend.
The service factory generates data management methods for your data models, so you don't have to implement these methods manually.
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">
@@ -54,7 +54,7 @@ In the example above, since the `HelloModuleService` extends `MedusaService`, it
### Generated Methods
The service factory generates data-management methods for each of the data models provided in the first parameter.
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`.