chore: reorganize docs apps (#7228)
* reorganize docs apps * add README * fix directory * add condition for old docs
This commit is contained in:
1 parent
224ebb2154
commit
4fe28f5a95
6187 files changed
+601447
-598226
No files matched your search
@@ -0,0 +1,56 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Database Connection Loader`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this document, you’ll learn about how to load the database connection in your module.
|
||||
|
||||
## Modules Database Connection
|
||||
|
||||
By default, a module will use the same database connection of the Medusa application.
|
||||
|
||||
To access those database credentials and ensure that MikroORM’s [entity](https://mikro-orm.io/docs/entity-manager) and [transaction](https://mikro-orm.io/docs/transactions) managers use those credentials when performing database operations, you must pass a database connection loader in your module’s definition.
|
||||
|
||||
Medusa provides a utility connection loader factory that handles registering the database options and entity and transaction managers into the module’s container.
|
||||
|
||||
---
|
||||
|
||||
## How to Add a Database Connection Loader?
|
||||
|
||||
In your module’s definition file, import the `ModulesSdkUtils` utility function from `@medusajs/utils`. Then, use its `mikroOrmConnectionLoaderFactory` function to create a connection loader and export it in the module’s definition.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/modules/hello/index.ts"
|
||||
// other imports...
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
|
||||
const pathToMigrations = __dirname + "/migrations"
|
||||
|
||||
// ...
|
||||
|
||||
const connectionLoader = ModulesSdkUtils
|
||||
.mikroOrmConnectionLoaderFactory({
|
||||
moduleName,
|
||||
moduleModels: [MyCustom],
|
||||
migrationsPath: pathToMigrations,
|
||||
})
|
||||
|
||||
export default {
|
||||
// ...
|
||||
loaders: [
|
||||
// ...
|
||||
connectionLoader,
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
The `mikroOrmConnectionLoaderFactory` function accepts an object with the following properties:
|
||||
|
||||
- `moduleName`: The name of the module.
|
||||
- `moduleModules`: An array of data models in the module.
|
||||
- `migrationsPath`: The path to the migrations file.
|
||||
|
||||
You can now implement data-management methods as explained in the next chapters.
|
||||
@@ -0,0 +1,118 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Module's Container`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you'll learn about a module's container and how to register resources in that container.
|
||||
|
||||
## Module's Container
|
||||
|
||||
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 resources registered in the module's container.
|
||||
|
||||
To register a module's resources in that container, Medusa provides a utility loader factory function that registers your module's services and other resources in the module's container.
|
||||
|
||||
---
|
||||
|
||||
## How to Register a Modules Resources
|
||||
|
||||
In your module's definition file, import the `ModulesSdkUtils` utility functions from the `@medusajs/utils`. Then, use its `moduleContainerLoaderFactory` function to create the container loader and export it in the module's definition.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/modules/hello/index.ts"
|
||||
// other imports...
|
||||
import HelloModuleService from "./service"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
import {
|
||||
ModulesSdkUtils,
|
||||
MikroOrmBaseRepository,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
// ...
|
||||
|
||||
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
|
||||
moduleModels: {
|
||||
MyCustom,
|
||||
},
|
||||
moduleRepositories: {
|
||||
BaseRepository: MikroOrmBaseRepository,
|
||||
},
|
||||
moduleServices: {
|
||||
HelloModuleService,
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
// ...
|
||||
loaders: [containerLoader],
|
||||
}
|
||||
```
|
||||
|
||||
### Container Loader Factory Parameters
|
||||
|
||||
The `moduleContainerLoaderFactory` function accepts as a parameter an object with the following properties:
|
||||
|
||||
- `moduleModels`: An object of the module's data models, where the key is the model's name and the value is the model class.
|
||||
- `moduleRepositories`: An object of the module's repositories. You import here the `MikroOrmBaseRepository` from `@medusajs/utils` and use it as the value of `BaseRepository`.
|
||||
- `moduleServices`: An object of the module's services, where the key is the service's registration name and the value is the service class.
|
||||
|
||||
### Resources Registered by the Container Loader
|
||||
|
||||
The container loader registers in the module's container:
|
||||
|
||||
- The services passed in the `moduleServices` property.
|
||||
- The repositories provided in the `moduleRepositories`.
|
||||
- A generated service for each data model provided in `moduleModels`.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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, to resolve the `baseRepository` registered by the `containerLoader` in the module's definition:
|
||||
|
||||
```ts
|
||||
import { DAL } from "@medusajs/types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
export default class HelloModuleService {
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
constructor({ baseRepository }: InjectedDependencies) {
|
||||
this.baseRepository_ = baseRepository
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Loader
|
||||
|
||||
A loader function in a module accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import {
|
||||
LoaderOptions,
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
|
||||
export default function helloWorldLoader({
|
||||
container,
|
||||
}: LoaderOptions) {
|
||||
const myCustomService = container.resolve("myCustomService")
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Database Operations in Service Methods`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this document, you’ll learn how to implement database operations, such as creating a record, in a service’s methods.
|
||||
|
||||
## Transaction Manager
|
||||
|
||||
A transaction wraps a set of database operations to ensure that when an error occurs, all database changes made by the executed operations are rolled back.
|
||||
|
||||
When you implement a method in your service, you must add the `@InjectTransactionManager` decorator to the method performing database operations. For example:
|
||||
|
||||
```ts title="src/modules/hello/service.ts" highlights={[["26"]]}
|
||||
// other imports
|
||||
import { InjectTransactionManager } from "@medusajs/utils"
|
||||
import { DAL } from "@medusajs/types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
myCustomService: ModulesSdkTypes.InternalModuleService<any>
|
||||
}
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory<
|
||||
// ...
|
||||
>(
|
||||
// ...
|
||||
) {
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
|
||||
constructor(
|
||||
{ baseRepository }: InjectedDependencies
|
||||
) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.baseRepository_ = baseRepository
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async create(
|
||||
// TODO add parameters
|
||||
) {
|
||||
// TODO add implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `@InjectTransactionManager` decorator accepts as a parameter the name of the service class field to inject the transaction manager to, which is the `baseRepository_` field.
|
||||
|
||||
---
|
||||
|
||||
## Generated Service Methods
|
||||
|
||||
As mentioned in a previous chapter, when you use the container loader factory to register resources in your module’s container, the factory creates a service for each data model you specify and registers it in the container. The registration name is the camel-case name of the data model with `Service` appended to it.
|
||||
|
||||
So, the container loader factory creates a `myCustomService` for the `MyCustom` data model.
|
||||
|
||||
These generated services already implement data-management methods such as `create` or `update`. So, in your module’s service, you can resolve the generated service and use it to create a record.
|
||||
|
||||
For example:
|
||||
|
||||
export const highlights = [
|
||||
["13", "", "Inject myCustomService, which is the service generated by the container loader for the MyCustom data model."],
|
||||
["23", "", "Add a new field for the generated service of the MyCustom data model."],
|
||||
["31", "", "Set the class field to the injected dependency."],
|
||||
["39", "create", "Use the `create` method of the generated service."]
|
||||
]
|
||||
|
||||
```ts title="src/modules/hello/service.ts" highlights={highlights}
|
||||
// other imports...
|
||||
import { ModulesSdkTypes } from "@medusajs/types"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
|
||||
// ...
|
||||
|
||||
type CreateMyCustomDTO = {
|
||||
name: string
|
||||
}
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
myCustomService: ModulesSdkTypes.InternalModuleService<any>
|
||||
}
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory<
|
||||
// ...
|
||||
>(
|
||||
// ...
|
||||
) {
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
protected myCustomService_: ModulesSdkTypes.InternalModuleService<MyCustom>
|
||||
|
||||
constructor(
|
||||
{ baseRepository, myCustomService }: InjectedDependencies
|
||||
) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.baseRepository_ = baseRepository
|
||||
this.myCustomService_ = myCustomService
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async create(
|
||||
data: CreateMyCustomDTO,
|
||||
@MedusaContext() context: Context = {}
|
||||
): Promise<MyCustomDTO> {
|
||||
const myCustom = await this.myCustomService_.create(
|
||||
data,
|
||||
context
|
||||
)
|
||||
|
||||
return myCustom
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, you add `myCustomService` to the `InjectedDependencies` type to resolve the service in the constructor and set it in a class field.
|
||||
|
||||
In the `create` method, you use `myCustomService`'s `create` method to create the record. You pass it the record’s data as a first parameter, and the Medusa application’s context as a second parameter.
|
||||
@@ -0,0 +1,411 @@
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent, TabsContentWrapper, Badge } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Example: CRUD Module Service`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll find an example of how to implement CRUD operations in your module’s service and test it out with an API route.
|
||||
|
||||
<Note>
|
||||
|
||||
The example in this chapter uses the same `hello` module with the `HelloModuleService` and `MyCustom` data model from previous chapters.
|
||||
|
||||
</Note>
|
||||
|
||||
## 1. Create Types File
|
||||
|
||||
Create the file `src/types/hello/my-custom.ts` with the following content:
|
||||
|
||||
```ts title="src/types/hello/my-custom.ts"
|
||||
export type MyCustomDTO = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type CreateMyCustomDTO = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type UpdateMyCustomDTO = {
|
||||
name?: string
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
You’ll be using these types in the service.
|
||||
|
||||
---
|
||||
|
||||
## 2. Create HelloModuleService
|
||||
|
||||
Create or change the service at `src/modules/hello/service.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/service.ts"
|
||||
import {
|
||||
ModulesSdkUtils,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
DAL,
|
||||
ModulesSdkTypes,
|
||||
Context,
|
||||
} from "@medusajs/types"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
import {
|
||||
CreateMyCustomDTO,
|
||||
MyCustomDTO,
|
||||
UpdateMyCustomDTO,
|
||||
} from "../../types/hello/my-custom"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
myCustomService: ModulesSdkTypes.InternalModuleService<any>
|
||||
}
|
||||
|
||||
type AllModelsDTO = {
|
||||
MyCustom: {
|
||||
dto: MyCustomDTO
|
||||
}
|
||||
}
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory<
|
||||
InjectedDependencies,
|
||||
MyCustomDTO,
|
||||
AllModelsDTO
|
||||
>(MyCustom, []) {
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
protected myCustomService_: ModulesSdkTypes.InternalModuleService<MyCustom>
|
||||
|
||||
constructor(
|
||||
{ baseRepository, myCustomService }: InjectedDependencies
|
||||
) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.baseRepository_ = baseRepository
|
||||
this.myCustomService_ = myCustomService
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async create(
|
||||
data: CreateMyCustomDTO,
|
||||
@MedusaContext() context: Context = {}
|
||||
): Promise<MyCustomDTO> {
|
||||
const myCustom = await this.myCustomService_.create(
|
||||
data,
|
||||
context
|
||||
)
|
||||
|
||||
return myCustom
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async update(
|
||||
id: string,
|
||||
data: UpdateMyCustomDTO,
|
||||
@MedusaContext() context: Context = {}
|
||||
): Promise<MyCustomDTO> {
|
||||
const myCustom = await this.myCustomService_.update({
|
||||
...data,
|
||||
id,
|
||||
})
|
||||
|
||||
return myCustom
|
||||
}
|
||||
}
|
||||
|
||||
export default HelloModuleService
|
||||
```
|
||||
|
||||
This creates a `HelloModuleService` that uses the service factory to generate data-management methods like `list`, `retrieve`, and `delete`.
|
||||
|
||||
<Note>
|
||||
|
||||
Refer to the full list of generated methods in [this chapter](../service-factory/page.mdx#generated-methods).
|
||||
|
||||
</Note>
|
||||
|
||||
In the service, you also implement the `create` and `update` methods that create and update a `MyCustom` record respectively.
|
||||
|
||||
---
|
||||
|
||||
## 3. Configure Module Definition
|
||||
|
||||
Make sure that the module definition at `src/modules/hello/index.ts` uses the container and connection loaders explained in previous chapters:
|
||||
|
||||
```ts title="src/modules/hello/index.ts"
|
||||
import HelloModuleService from "./service"
|
||||
import { ModulesSdkUtils, MikroOrmBaseRepository } from "@medusajs/utils"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
|
||||
const moduleName = "hello"
|
||||
const pathToMigrations = __dirname + "/migrations"
|
||||
|
||||
const migrationScriptOptions = {
|
||||
moduleName,
|
||||
models: {
|
||||
MyCustom,
|
||||
},
|
||||
pathToMigrations,
|
||||
}
|
||||
|
||||
export const runMigrations = ModulesSdkUtils
|
||||
.buildMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
|
||||
export const revertMigration = ModulesSdkUtils
|
||||
.buildRevertMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
|
||||
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
|
||||
moduleModels: {
|
||||
MyCustom,
|
||||
},
|
||||
moduleRepositories: {
|
||||
BaseRepository: MikroOrmBaseRepository,
|
||||
},
|
||||
moduleServices: [HelloModuleService],
|
||||
})
|
||||
|
||||
const connectionLoader = ModulesSdkUtils.mikroOrmConnectionLoaderFactory({
|
||||
moduleName,
|
||||
moduleModels: [MyCustom],
|
||||
migrationsPath: pathToMigrations,
|
||||
})
|
||||
|
||||
export default {
|
||||
service: HelloModuleService,
|
||||
runMigrations,
|
||||
revertMigration,
|
||||
loaders: [containerLoader, connectionLoader],
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
|
||||
If you don't have the `MyCustom` data model, refer to [this chapter](../../../basics/data-models/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
Also, make sure that the module is added in the `modules` object defined in `medusa-config.js`:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
const modules = {
|
||||
helloModuleService: {
|
||||
resolve: "./dist/modules/hello",
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Add List and Create API Routes
|
||||
|
||||
Create the file `src/api/store/custom/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/store/custom/route.ts"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
|
||||
import HelloModuleService from "../../../modules/hello/service"
|
||||
import { CreateMyCustomDTO } from "../../../types/hello/my-custom"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService =
|
||||
req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
res.json({
|
||||
my_customs: await helloModuleService.list(),
|
||||
})
|
||||
}
|
||||
|
||||
type CustomReq = CreateMyCustomDTO
|
||||
|
||||
export async function POST(
|
||||
req: MedusaRequest<CustomReq>,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService =
|
||||
req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
// skipping validation for simplicity
|
||||
const myCustom = await helloModuleService.create(req.body)
|
||||
|
||||
res.json({
|
||||
my_custom: myCustom,
|
||||
})
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This adds two API routes:
|
||||
|
||||
1. A `GET` API route at `/store/custom` that retrieves a list of `MyCustom` records.
|
||||
2. A `POST` API route at `/store/custom` that creates a `MyCustom` record.
|
||||
|
||||
---
|
||||
|
||||
## 5. Add Retrieve, Update, and Delete API Routes
|
||||
|
||||
Create the file `src/api/store/custom/[id]/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/store/custom/[id]/route.ts"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
|
||||
import HelloModuleService from "../../../../modules/hello/service"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService =
|
||||
req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
const myCustom = await helloModuleService.retrieve(req.params.id)
|
||||
|
||||
res.json({
|
||||
my_custom: myCustom,
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService =
|
||||
req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
// skipping validation for simplicity
|
||||
const myCustom = await helloModuleService.update(
|
||||
req.params.id,
|
||||
req.body
|
||||
)
|
||||
|
||||
res.json({
|
||||
my_custom: myCustom,
|
||||
})
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService =
|
||||
req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
await helloModuleService.delete(req.params.id)
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This creates three API routes:
|
||||
|
||||
1. A `GET` API route at `/store/custom/[id]` that retrieves a `MyCustom` record by its ID.
|
||||
2. A `POST` API route at `/store/custom/[id]` that updates a `MyCustom` record.
|
||||
3. A `DELETE` API route at `/store/custom/[id]` that deletes a `MyCustom` record.
|
||||
|
||||
---
|
||||
|
||||
## 6. Test it Out
|
||||
|
||||
Start your Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, test out each of the API routes you created.
|
||||
|
||||
<Tabs defaultValue="list-route" layoutType="vertical">
|
||||
<TabsList>
|
||||
<TabsTrigger value="list-route">
|
||||
|
||||
<Badge variant="green">GET</Badge> /store/custom
|
||||
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="create-route">
|
||||
|
||||
<Badge variant="blue">POST</Badge> /store/custom
|
||||
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="retrieve-route">
|
||||
|
||||
<Badge variant="green">GET</Badge> /store/custom/[id]
|
||||
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="update-route">
|
||||
|
||||
<Badge variant="blue">POST</Badge> /store/custom/[id]
|
||||
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="delete-route">
|
||||
|
||||
<Badge variant="red">DEL</Badge> /store/custom/[id]
|
||||
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContentWrapper>
|
||||
<TabsContent value="list-route">
|
||||
|
||||
```bash apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/custom"
|
||||
curl http://localhost:9000/store/custom
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="create-route">
|
||||
|
||||
```bash apiTesting testApiMethod="POST" testApiUrl="http://localhost:9000/store/custom" testBodyParams={{ "name": "test" }}
|
||||
curl -X POST http://localhost:9000/store/custom \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"name": "test"
|
||||
}'
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="retrieve-route">
|
||||
|
||||
```bash apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/custom/{id}" testPathParams={{ "id": "mc_123" }}
|
||||
curl http://localhost:9000/store/custom/mc_123
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="update-route">
|
||||
|
||||
```bash apiTesting testApiMethod="POST" testApiUrl="http://localhost:9000/store/custom/{id}" testPathParams={{ "id": "mc_123" }} testBodyParams={{ "name": "test" }}
|
||||
curl -X POST http://localhost:9000/store/custom/mc_123 \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"name": "test"
|
||||
}'
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="delete-route">
|
||||
|
||||
```bash apiTesting testApiMethod="DELETE" testApiUrl="http://localhost:9000/store/custom/{id}" testPathParams={{ "id": "mc_123" }}
|
||||
curl -X DELETE http://localhost:9000/store/custom/mc_123
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
</TabsContentWrapper>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,431 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Example: Module Relationship`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
This chapter gives an example of adding a relationship from your custom module to the Product Module.
|
||||
|
||||
<Note>
|
||||
|
||||
The example in this chapter uses the same `hello` module with the `HelloModuleService` from previous chapters.
|
||||
|
||||
</Note>
|
||||
|
||||
## Create CustomProductData Data Model
|
||||
|
||||
Create the file `src/modules/hello/models/custom-product-data.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/models/custom-product-data.ts"
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
@Entity()
|
||||
export class CustomProductData {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
custom_field: string
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
product_id?: string
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "cpd")
|
||||
}
|
||||
|
||||
@OnInit()
|
||||
OnInit() {
|
||||
this.id = generateEntityId(this.id, "cpd")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This creates a new data model `CustomProductData` to the `hello` module that stores custom fields related to the `Product` data model in the Product Module
|
||||
|
||||
### Add Data Model in MikroORM Configurations
|
||||
|
||||
Add your new data model to the MikroORM configurations file at `src/modules/hello/mikro-orm.config.dev.ts`:
|
||||
|
||||
```ts title="src/modules/hello/mikro-orm.config.dev.ts"
|
||||
// other imports...
|
||||
import { CustomProductData } from "./models/custom-product-data"
|
||||
|
||||
module.exports = {
|
||||
entities: [
|
||||
// other data models...
|
||||
CustomProductData,
|
||||
],
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Add Data Model to Module Definition
|
||||
|
||||
In your module definition at `src/modules/hello/index.ts`, add the data model to the `models` object:
|
||||
|
||||
```ts title="src/modules/hello/index.ts"
|
||||
// other imports...
|
||||
import { CustomProductData } from "./models/custom-product-data"
|
||||
|
||||
const models = {
|
||||
// other models...
|
||||
CustomProductData,
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
|
||||
moduleModels: models,
|
||||
// ...
|
||||
})
|
||||
|
||||
const connectionLoader = ModulesSdkUtils.mikroOrmConnectionLoaderFactory({
|
||||
moduleModels: Object.values(models),
|
||||
// ...
|
||||
})
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
This ensures that the model is passed to the container and connection loader along with other models.
|
||||
|
||||
### Add Data Model Alias in Joiner Configurations
|
||||
|
||||
Add an alias for the data model in the joiner configurations defined in `src/modules/hello/joiner-config.ts`:
|
||||
|
||||
```ts title="src/modules/hello/joiner-config.ts"
|
||||
// other imports...
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { CustomProductData } from "./models/custom-product-data"
|
||||
|
||||
const joinerConfig: ModuleJoinerConfig = {
|
||||
// ...,
|
||||
alias: [
|
||||
// other aliases
|
||||
{
|
||||
name: ["custom_product_data"],
|
||||
args: {
|
||||
entity: CustomProductData.name,
|
||||
methodSuffix: "CustomProductDatas",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### Create Types
|
||||
|
||||
Create the file `src/types/hello/custom-product-data.ts` with the following types:
|
||||
|
||||
```ts title="src/types/hello/custom-product-data.ts"
|
||||
export type CustomProductDataDTO = {
|
||||
id: string
|
||||
custom_field: string
|
||||
product_id?: string
|
||||
}
|
||||
|
||||
export type CreateCustomProductDataDTO = {
|
||||
custom_field: string
|
||||
product_id?: string
|
||||
}
|
||||
|
||||
export type UpdateCustomProductDataDTO = {
|
||||
custom_field?: string
|
||||
product_id?: string
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
You’ll use those in the service next.
|
||||
|
||||
### Add Data Model to Main Module Service
|
||||
|
||||
In the main module service defined at `src/modules/hello/service.ts`, make the following changes to generate and add methods for the new `CustomProductData` model:
|
||||
|
||||
export const serviceHighlights = [
|
||||
["10", "customProductDataService", "Inject the generated service for the `CustomProductData` data model."],
|
||||
["16", "CustomProductData", "Specify the expected input/output type of the data model."],
|
||||
["23", "CustomProductData", "Specify the `CustomProductData` data model to generate methods for it."],
|
||||
["49", "", "Set the `customProductDataService_` class field to the injected `customProductDataService` service."],
|
||||
["55", "createCustomProductData", "Add a method that creates a `CustomProductData` record."]
|
||||
]
|
||||
|
||||
```ts title="src/modules/hello/service.ts" highlights={serviceHighlights}
|
||||
// other imports...
|
||||
import { CustomProductData } from "./models/custom-product-data"
|
||||
import {
|
||||
CreateCustomProductDataDTO,
|
||||
CustomProductDataDTO,
|
||||
} from "../../types/hello/custom-product-data"
|
||||
|
||||
type InjectedDependencies = {
|
||||
// other injected dependencies...
|
||||
customProductDataService:
|
||||
ModulesSdkTypes.InternalModuleService<any>
|
||||
}
|
||||
|
||||
type AllModelsDTO = {
|
||||
// other model DTOs...
|
||||
CustomProductData: {
|
||||
dto: CustomProductDataDTO
|
||||
}
|
||||
}
|
||||
|
||||
const generateMethodsFor = [
|
||||
// other data models generating methods for...
|
||||
CustomProductData,
|
||||
]
|
||||
|
||||
// ...
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory<
|
||||
InjectedDependencies,
|
||||
MyCustomDTO,
|
||||
AllModelsDTO
|
||||
>(MyCustom, generateMethodsFor) {
|
||||
// ...
|
||||
protected customProductDataService_:
|
||||
ModulesSdkTypes.InternalModuleService<CustomProductData>
|
||||
|
||||
constructor(
|
||||
{
|
||||
// other injected dependencies...
|
||||
customProductDataService,
|
||||
}: InjectedDependencies,
|
||||
protected readonly moduleDeclaration:
|
||||
InternalModuleDeclaration
|
||||
) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
// ...
|
||||
this.customProductDataService_ = customProductDataService
|
||||
}
|
||||
|
||||
// other methods...
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async createCustomProductData(
|
||||
data: CreateCustomProductDataDTO,
|
||||
@MedusaContext() context: Context = {}
|
||||
): Promise<CustomProductDataDTO> {
|
||||
const customProductData = await this.customProductDataService_.create(
|
||||
data,
|
||||
context
|
||||
)
|
||||
|
||||
return this.baseRepository_.serialize<CustomProductDataDTO>(
|
||||
customProductData
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the main service you make the following changes:
|
||||
|
||||
1. Add `customProductDataService` to the list of injected dependencies.
|
||||
2. Add the expected input/output type of `CustomProductData` into the `AllModelsDTO` type, which is passed as the third-argument to the `abstractModuleServiceFactory` function.
|
||||
3. Add the data model to the `generateMethodsFor` array, which is passed as a second parameter to the `abstractModuleServiceFactory` function.
|
||||
4. Add a `customProductDataService_` field to the `HelloModuleService` and set its value in the constructor to the injected `customProductDataService`.
|
||||
5. Create a new method `createCustomProductData` that creates a `CustomProductData` record.
|
||||
|
||||
### Generate Migration
|
||||
|
||||
Use the following command to generate the migration file:
|
||||
|
||||
```bash
|
||||
npx cross-env MIKRO_ORM_CLI=./src/modules/hello/mikro-orm.config.dev.ts mikro-orm migration:create
|
||||
```
|
||||
|
||||
### Run Migration
|
||||
|
||||
Run the following commands to run the latest migration:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run build
|
||||
npx medusa migrations run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Add Relationship
|
||||
|
||||
To add the relationship from the `hello` module to the Product Module, make the following change in `src/modules/hello/joiner-config.ts`:
|
||||
|
||||
```ts title="src/modules/hello/joiner-config.ts"
|
||||
// other imports...
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
|
||||
const joinerConfig: ModuleJoinerConfig = {
|
||||
// ...
|
||||
relationships: [
|
||||
{
|
||||
serviceName: Modules.PRODUCT,
|
||||
primaryKey: "id",
|
||||
foreignKey: "product_id",
|
||||
alias: "product",
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
This adds a new relationship in the joiner configuration of the `hello` module. The relationship references the `product` alias in the Product Module, which is the alias of the `Product` data model in that module.
|
||||
|
||||
The `product_id` field in your module’s data models references the `id` field of the `Product` data model.
|
||||
|
||||
---
|
||||
|
||||
## Implement Create API Route
|
||||
|
||||
In this section, you’ll implement an API route that creates a product (for simplicity) and then creates a `CustomProductData` record that references that product.
|
||||
|
||||
<Note title="Good to know">
|
||||
|
||||
This example creates a product in a store route to simplify testing the relationship. In a realistic use case, only create products under the `/admin` prefix to ensure that the user is an authenticated admin.
|
||||
|
||||
</Note>
|
||||
|
||||
Create the file `src/api/store/custom-product-data/route.ts` with the following content:
|
||||
|
||||
export const apiRouteHighlights = [
|
||||
["37", "remoteQuery", "Resolve the remote query function from the Medusa container."],
|
||||
["44", "", "Create a product using the parameters in the request body."],
|
||||
["48", "", "Create a `CustomProductData` record with a reference to the created product's ID."],
|
||||
["54", "remoteQueryObjectFromString", "Create the query to pass to the remote query function."],
|
||||
["59", '"product.title"', "Retrieve the product title among the `CustomProductData` record's data."],
|
||||
["60", '"product.handle"', "Retrieve the product handle among the `CustomProductData` record's data."],
|
||||
["63", "", "Filter fetched records by the ID of the created record."],
|
||||
["67", "", "Run the query with the remote query function."],
|
||||
["70", "", "Return the created `CustomProductData` record in the response."]
|
||||
]
|
||||
|
||||
```ts title="src/api/store/custom-product-data/route.ts" highlights={apiRouteHighlights}
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
|
||||
import {
|
||||
CreateCustomProductDataDTO,
|
||||
} from "../../../types/hello/custom-product-data"
|
||||
import HelloModuleService from "../../../modules/hello/service"
|
||||
import type {
|
||||
IProductModuleService,
|
||||
CreateProductDTO,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
remoteQueryObjectFromString,
|
||||
ContainerRegistrationKeys,
|
||||
} from "@medusajs/utils"
|
||||
import type {
|
||||
RemoteQueryFunction,
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
type CreateCustomProductDataReq =
|
||||
CreateCustomProductDataDTO & {
|
||||
product_data: CreateProductDTO
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
req: MedusaRequest<CreateCustomProductDataReq>,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService =
|
||||
req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
const productModuleService: IProductModuleService =
|
||||
req.scope.resolve(
|
||||
"productModuleService"
|
||||
)
|
||||
|
||||
const remoteQuery: RemoteQueryFunction = req.scope.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_QUERY
|
||||
)
|
||||
|
||||
// skipping validation for simplicity
|
||||
const { product_data: productData, ...rest } = req.body
|
||||
|
||||
const product = await productModuleService.create(
|
||||
productData as CreateProductDTO
|
||||
)
|
||||
|
||||
const customProductData = await helloModuleService
|
||||
.createCustomProductData({
|
||||
...rest,
|
||||
product_id: product.id,
|
||||
})
|
||||
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "custom_product_data",
|
||||
fields: [
|
||||
"id",
|
||||
"custom_field",
|
||||
"product.title",
|
||||
"product.handle",
|
||||
],
|
||||
variables: {
|
||||
id: customProductData.id,
|
||||
},
|
||||
})
|
||||
|
||||
const results = await remoteQuery(query)
|
||||
|
||||
res.json({
|
||||
custom_product_data: results[0],
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This API route accepts the necessary request body parameters to create a `CustomProductData` record. It also accepts a `product_data` request body parameter to create the product to be referenced by the `CustomProductData` record.
|
||||
|
||||
In the API route handler, you create the product and then create the `CustomProductData` record.
|
||||
|
||||
Then, you use the remote query function, resolved from the Medusa container, to fetch the created `CustomProductData` record (filtering by its ID) with the product it references. You can add more product-related fields to the `fields` array passed to `remoteQueryObjectFromString`.
|
||||
|
||||
Finally, you return the data returned by the `remoteQuery` function.
|
||||
|
||||
### Test Create API Route
|
||||
|
||||
To test the API route, start the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, send a `POST` request to the `/store/custom-product-data` route:
|
||||
|
||||
```bash
|
||||
curl --location 'http://localhost:9000/store/custom-product-data' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"product_data": {
|
||||
"title": "Pants"
|
||||
},
|
||||
"custom_field": "nice shirt"
|
||||
}'
|
||||
```
|
||||
|
||||
You’ll receive the following response:
|
||||
|
||||
```json
|
||||
{
|
||||
"custom_product_data": {
|
||||
"id": "cpd_01HWWDD260T0CS64WAF8CVHQ9Y",
|
||||
"custom_field": "nice shirt",
|
||||
"product_id": "prod_01HWWDD25VPQNNZJ57PFTX2JHE",
|
||||
"product": {
|
||||
"title": "Shoes",
|
||||
"handle": "shoes",
|
||||
"id": "prod_01HWWDD25VPQNNZJ57PFTX2JHE"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,146 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Link Modules`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn what a link module is and how to use the remote link in your customizations.
|
||||
|
||||
## What is a Link Module?
|
||||
|
||||
A link module is a module whose only purpose is to define a relationship between two modules’ data models. The relationship is represented as a pivot or link table in the database, pointing at the primary keys of each data model.
|
||||
|
||||
For example, Medusa defines a link module between the Product and Pricing modules. The link module builds a relationship between the `ProductVariant` data model and the `PriceSet` data model.
|
||||
|
||||

|
||||
|
||||
Link modules provide more flexibility in managing relationships between modules while maintaining module isolation.
|
||||
|
||||
Link modules are currently only available for Medusa’s commerce modules. The Medusa application only creates the link tables when both modules are available.
|
||||
|
||||
---
|
||||
|
||||
## What is the Remote Link?
|
||||
|
||||
The remote link is a class with utility methods to manage links between modules. It’s registered in the Medusa container under the `remoteLink` registration name.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
ModuleRegistrationName,
|
||||
RemoteLink,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
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/utils"
|
||||
|
||||
// ...
|
||||
|
||||
await remoteLink.create({
|
||||
[Modules.PRODUCT]: {
|
||||
variant_id: product.variants[0].id,
|
||||
},
|
||||
[Modules.PRICING]: {
|
||||
price_set_id: price.id,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules.
|
||||
|
||||
The value of each module’s property is an object. It defines the values of the linked fields.
|
||||
|
||||
So, in the example above, you specify for the Product Module the value of the `variant_id`, and for the Pricing Module the value of `price_set_id`. These are the fields linked between the models of the two modules.
|
||||
|
||||
### Dismiss Link
|
||||
|
||||
To remove a link between records of two data models, use the `dismiss` method of the remote link. This doesn’t remove the records, only the relation between them.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { Modules } from "@medusajs/utils"
|
||||
|
||||
// ...
|
||||
|
||||
await remoteLink.dismiss({
|
||||
[Modules.PRODUCT]: {
|
||||
variant_id: product.variants[0].id,
|
||||
},
|
||||
[Modules.PRICING]: {
|
||||
price_set_id: price.id,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The `dismiss` method accepts the same parameter type as the create method.
|
||||
|
||||
### Cascade Delete Linked Records
|
||||
|
||||
If a record, such as a variant, is deleted, use the `delete` method of the remote link to delete all associated links with cascade delete enabled.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { Modules } from "@medusajs/utils"
|
||||
|
||||
// ...
|
||||
|
||||
await productModuleService.deleteVariants([variant.id])
|
||||
|
||||
await remoteLink.delete({
|
||||
[Modules.PRODUCT]: {
|
||||
variant_id: variant.id,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
This deletes all records linked to the deleted variant with cascade delete enabled in their relationship.
|
||||
|
||||
### Restore Linked Records
|
||||
|
||||
If a record, such as a variant, that was previously soft-deleted is now restored, use the `restore` method of the remote link to restore all associated links that were cascade deleted.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { Modules } from "@medusajs/utils"
|
||||
|
||||
// ...
|
||||
|
||||
await productModuleService.restoreVariants([variant.id])
|
||||
|
||||
await remoteLink.restore({
|
||||
[Modules.PRODUCT]: {
|
||||
variant_id: variant.id,
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,239 @@
|
||||
import { TypeList, CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Module Relationships`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this document, you’ll learn about creating relationships in queryable modules.
|
||||
|
||||
## What is a Relationship in a Queryable Module?
|
||||
|
||||
A queryable module can have a relationship to another queryable module in the form of a reference.
|
||||
|
||||
The Medusa application resolves these relationships while maintaining isolation between the modules and allowing you to retrieve data across them using the remote query.
|
||||
|
||||
<Note title="Use module relationships when" type="success">
|
||||
|
||||
- You want to build relationships between the data models of modules.
|
||||
- You want to assoaciate more fields with the data model of another module.
|
||||
|
||||
</Note>
|
||||
|
||||
<Note title="Don't use module relationships if" type="error">
|
||||
|
||||
- You’re building relationships between data models in the same module. Use foreign keys instead.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Module Relationship?
|
||||
|
||||
<Note title="Tip">
|
||||
|
||||
The next chapter provides a detailed example of implementing a relationship between two modules. This section gives the general steps to creating the relationship.
|
||||
|
||||
</Note>
|
||||
|
||||
Consider you’re creating a data model that adds custom fields associated with a product:
|
||||
|
||||
```ts title="src/modules/hello/models/another-custom.ts" highlights={[["13"]]}
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
@Entity()
|
||||
export class CustomProductData {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
custom_field: string
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
product_id?: string
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "cpd")
|
||||
}
|
||||
|
||||
@OnInit()
|
||||
OnInit() {
|
||||
this.id = generateEntityId(this.id, "cpd")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `CustomProductData` data model has a `product_id` field to reference the product it adds custom fields for.
|
||||
|
||||
To create a relationship to the `product` data model in the Product Module, add the following to the joiner configuration of your module:
|
||||
|
||||
export const relationshipsHighlight = [
|
||||
["10", "serviceName", "The name of the module that this relationship is referencing."],
|
||||
["11", "alias", "The alias of the data model you’re referencing in the other module."],
|
||||
["12", "primaryKey", "The name of the field you’re referencing in the other module’s data model."],
|
||||
["13", "foreignKey", "The name of the field in your data models referencing the other module’s model."],
|
||||
]
|
||||
|
||||
```ts title="src/modules/hello/joiner-config.ts" highlights={relationshipsHighlight}
|
||||
// other imports...
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { CustomProductData } from "./models/custom-product-data"
|
||||
|
||||
const joinerConfig: ModuleJoinerConfig = {
|
||||
// ...
|
||||
relationships: [
|
||||
{
|
||||
serviceName: Modules.PRODUCT,
|
||||
alias: "product",
|
||||
primaryKey: "id",
|
||||
foreignKey: "product_id",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default joinerConfig
|
||||
```
|
||||
|
||||
You pass a new property `relationships` to the `joinerConfig`. This property’s value is an array of relationship definitions from your module to other modules.
|
||||
|
||||
Each relationship definition object accepts the following properties:
|
||||
|
||||
<TypeList types={[
|
||||
{
|
||||
name: "serviceName",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The name of the module (as added in `medusa-config.js`) that this relationship is referencing. If you’re referencing a Medusa commerce module, use the `Modules` enum imported from `@medusajs/modules-sdk`."
|
||||
},
|
||||
{
|
||||
name: "alias",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The alias of the data model you’re referencing in the other module. The alias is defined in the joiner configuration of that module, similar to your module."
|
||||
},
|
||||
{
|
||||
name: "primaryKey",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The name of the field you’re referencing in the other module’s data model."
|
||||
},
|
||||
{
|
||||
name: "foreignKey",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The name of the field in your data models referencing the other module’s model."
|
||||
}
|
||||
]} />
|
||||
|
||||
So, the above example creates a relationship to the `Product` data model of the Product Module, which has the alias `product`. `product_id` fields in your module’s data models are considered references to the `id` field of the `Product` data model in the Product Module.
|
||||
|
||||
---
|
||||
|
||||
## Query Data Across Relationships
|
||||
|
||||
The remote query allows you to query data across modules using their relationship without the actual dependency between the modules.
|
||||
|
||||
To do that, specify within the `fields` retrieved in the query the referenced data model’s fields. For example:
|
||||
|
||||
```ts highlights={[["3", '"product.title"', "Retrieve the referenced product's title."]]}
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "another_custom",
|
||||
fields: ["id", "custom_field", "product.title"],
|
||||
})
|
||||
|
||||
const result = await remoteQuery(query)
|
||||
```
|
||||
|
||||
Use `alias`'s value in the relationship definition to reference the other module’s data model. To specify its fields, use dot notation.
|
||||
|
||||
So, in the example above, the referenced product’s title is retrieved along with the ID under the `product` property of each record object in the result.
|
||||
|
||||
---
|
||||
|
||||
## Reference Inner Data Models
|
||||
|
||||
If the data model you’re referencing isn’t the main data model of the main module service, pass to the relationship definition the `args` property:
|
||||
|
||||
```ts title="src/modules/hello/joiner-config.ts" highlights={[["10", "methodSuffix", "The suffix of the referenced data model's methods."]]}
|
||||
const joinerConfig: ModuleJoinerConfig = {
|
||||
// ...
|
||||
relationships: [
|
||||
{
|
||||
serviceName: Modules.PRODUCT,
|
||||
primaryKey: "id",
|
||||
foreignKey: "variant_id",
|
||||
alias: "variant",
|
||||
args: {
|
||||
methodSuffix: "Variants",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
The `args` property’s value is an object accepting a `methodSuffix` property. The `methodSuffix` property’s value is the plural name of the data model.
|
||||
|
||||
---
|
||||
|
||||
## Inverse Relationship
|
||||
|
||||
If you’re creating a relationship between two custom modules, you can define the relationship on the referenced side using the `extends` property of the joiner configuration.
|
||||
|
||||
For example:
|
||||
|
||||
<CodeTabs groupId="inverse-relation">
|
||||
<CodeTab label="Hello Module" value="hello-module">
|
||||
|
||||
```ts title="src/modules/hello/joiner-config.ts"
|
||||
// Hello module
|
||||
const joinerConfig: ModuleJoinerConfig = {
|
||||
// ...
|
||||
relationships: [
|
||||
{
|
||||
serviceName: "anotherHelloModuleService",
|
||||
alias: "another_custom",
|
||||
primaryKey: "id",
|
||||
foreignKey: "another_custom_id",
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="Another Hello Module" value="another-hello-module">
|
||||
|
||||
```ts title="src/modules/another-hello/joiner-config.ts" highlights={[["6", "serviceName", "The name of the module this relationship was originally created in."], ["7", "relationship", "The relationship object as defined in the referencing module."]]}
|
||||
// Another hello module
|
||||
const joinerConfig: ModuleJoinerConfig = {
|
||||
// ...
|
||||
extends: [
|
||||
{
|
||||
serviceName: "helloModuleService",
|
||||
relationship: {
|
||||
serviceName: "anotherHelloModuleService",
|
||||
alias: "another_custom",
|
||||
primaryKey: "id",
|
||||
foreignKey: "another_custom_id",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
The `extends` property’s value is an array of objects having the following properties:
|
||||
|
||||
1. `serviceName`: The name of the module this relationship was originally created in.
|
||||
2. `relationship`: The relationship object as defined in the referencing module.
|
||||
@@ -0,0 +1,147 @@
|
||||
import { CodeTabs, CodeTab } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Module Options`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about passing options to your Module from the Medusa application’s configurations and using them in the Module’s resources.
|
||||
|
||||
## What are Module Options?
|
||||
|
||||
A module can receive options to customize or configure its functionality.
|
||||
|
||||
For example, if you’re creating a module that integrates a third-party service, you’ll want to receive the integration credentials in the options rather than adding them directly in your code.
|
||||
|
||||
A module can receive options when they’re added to the Medusa application’s configuration.
|
||||
|
||||
---
|
||||
|
||||
## How to Pass Options to a Module?
|
||||
|
||||
To pass options to a module, add an `options` property to the module’s configuration in `medusa-config.js`.
|
||||
|
||||
For example:
|
||||
|
||||
```js title="medusa-config.js"
|
||||
const modules = {
|
||||
helloModuleService: {
|
||||
resolve: "./dist/modules/hello",
|
||||
options: {
|
||||
capitalize: true,
|
||||
},
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The `options` property’s value is an object. You can pass any properties you want.
|
||||
|
||||
---
|
||||
|
||||
## Access Module Options in Main Service
|
||||
|
||||
The module’s main service receives the module’s declaration as a second parameter. The module declaration is an object having an `options` property. Use it to access the options passed from the Module’s configurations.
|
||||
|
||||
For example:
|
||||
|
||||
<CodeTabs groupId="service-type">
|
||||
<CodeTab label="Using Service Factory" value="service-factory">
|
||||
|
||||
```ts highlights={[["24"], ["28"], ["29"], ["30"]]}
|
||||
// other imports...
|
||||
import { InternalModuleDeclaration } from "@medusajs/types"
|
||||
|
||||
// ...
|
||||
|
||||
// recommended to define type in another file
|
||||
type HelloModuleOptions = {
|
||||
capitalize?: boolean
|
||||
}
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory<
|
||||
// ...
|
||||
>(
|
||||
// ...
|
||||
) {
|
||||
// ...
|
||||
protected options_: HelloModuleOptions
|
||||
|
||||
constructor(
|
||||
{
|
||||
// ...
|
||||
}: InjectedDependencies,
|
||||
protected readonly moduleDeclaration: InternalModuleDeclaration
|
||||
) {
|
||||
//...
|
||||
|
||||
this.options_ = moduleDeclaration.options || {
|
||||
capitalize: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
<CodeTab label="Without Service Factory" value="no-service-factory">
|
||||
|
||||
```ts highlights={[["9"], ["13"], ["14"], ["16"], ["17"], ["18"]]}
|
||||
import { InternalModuleDeclaration } from "@medusajs/types"
|
||||
|
||||
// recommended to define type in another file
|
||||
type HelloModuleOptions = {
|
||||
capitalize?: boolean
|
||||
}
|
||||
|
||||
export default class HelloModuleService {
|
||||
protected options_: HelloModuleOptions
|
||||
|
||||
constructor(
|
||||
{},
|
||||
protected readonly moduleDeclaration:
|
||||
InternalModuleDeclaration
|
||||
) {
|
||||
this.options_ = moduleDeclaration.options || {
|
||||
capitalize: false,
|
||||
}
|
||||
}
|
||||
|
||||
getMessage() {
|
||||
return "Hello, world!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</CodeTab>
|
||||
</CodeTabs>
|
||||
|
||||
---
|
||||
|
||||
## Access Module Options in Loader
|
||||
|
||||
The object that a module’s loaders receive as a parameter has an `options` property. Use it to access the options passed from the Module’s configurations.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/modules/hello/loaders/hello-world.ts" highlights={[["11"], ["16"]]}
|
||||
import {
|
||||
LoaderOptions,
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
// recommended to define type in another file
|
||||
type HelloModuleOptions = {
|
||||
capitalize?: boolean
|
||||
}
|
||||
|
||||
export default function helloWorldLoader({
|
||||
options,
|
||||
}: LoaderOptions<HelloModuleOptions>) {
|
||||
|
||||
console.log(
|
||||
"[HELLO MODULE] Just started the Medusa application!",
|
||||
options
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,178 @@
|
||||
import { TypeList } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Queryable Modules`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn what a queryable module is and how to configure a module to be queryable.
|
||||
|
||||
## What is a Queryable Module?
|
||||
|
||||
A queryable module is one whose data can be queried more flexibly and referenced in other modules.
|
||||
|
||||
By making a module queryable, you can fetch its data using object and GraphQL syntax without using the main service’s methods.
|
||||
|
||||
<Note title="Use queryable modules when" type="success">
|
||||
|
||||
- You want more flexibility when fetching data.
|
||||
- You’re creating relations between modules and querying data across modules.
|
||||
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## How to Make a Module Queryable
|
||||
|
||||
<Note title="Steps Summary">
|
||||
|
||||
1. Configure module to be queryable.
|
||||
2. Add `__joinerConfig` method to main service.
|
||||
|
||||
</Note>
|
||||
|
||||
### 1. Adjust Module Configuration
|
||||
|
||||
To make a module queryable, adjust its configuration object passed to the `modules` object in `medusa-config.js`. The configuration object accepts a `definition` property, whose value is an object of advanced module configurations.
|
||||
|
||||
For example:
|
||||
|
||||
export const configHighlights = [
|
||||
["5", "key", "The module's key in the `modules` object."],
|
||||
["6", "registrationName", "The name that the main service is registered under in the Medusa container. It’s recommended to be the same as `key`'s value."],
|
||||
["7", "isQueryable", "Whether the module is queryable."]
|
||||
]
|
||||
|
||||
```js title="medusa-config.js" highlights={configHighlights}
|
||||
const modules = {
|
||||
helloModuleService: {
|
||||
resolve: "./dist/modules/hello",
|
||||
definition: {
|
||||
key: "helloModuleService",
|
||||
registrationName: "helloModuleService",
|
||||
isQueryable: true,
|
||||
},
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The `definition` property’s value accepts the following properties, among others:
|
||||
|
||||
<TypeList types={[
|
||||
{
|
||||
name: "key",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The module's key in the `modules` object."
|
||||
},
|
||||
{
|
||||
name: "registrationName",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The name that the main service is registered under in the Medusa container. It’s recommended to be the same as `key`'s value.",
|
||||
},
|
||||
{
|
||||
name: "isQueryable",
|
||||
type: "`boolean`",
|
||||
description: "Whether the module is queryable."
|
||||
}
|
||||
]} sectionTitle="Adjust Module Configuration" />
|
||||
|
||||
### 2. Define `__joinerConfig` Method
|
||||
|
||||
Queryable modules must define a public `__joinerConfig` method in their main service. This method returns an object that defines how a module’s data can be accessed and referenced.
|
||||
|
||||
Create the file `src/modules/hello/joiner-config.ts` with the following content:
|
||||
|
||||
```ts title="src/modules/hello/joiner-config.ts"
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
|
||||
const joinerConfig: ModuleJoinerConfig = {
|
||||
serviceName: "helloModuleService",
|
||||
primaryKeys: ["id"],
|
||||
alias: [
|
||||
{
|
||||
name: ["my_custom"],
|
||||
args: {
|
||||
entity: MyCustom.name,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default joinerConfig
|
||||
```
|
||||
|
||||
The `joinerConfig` object of type `ModuleJoinerConfig` imported from `@medusajs/types` accepts the following properties:
|
||||
|
||||
<TypeList types={[
|
||||
{
|
||||
name: "serviceName",
|
||||
type: "`string`",
|
||||
optional: false,
|
||||
description: "The name of the main module service as registered in the Medusa container."
|
||||
},
|
||||
{
|
||||
name: "primaryKeys",
|
||||
type: "`string[]`",
|
||||
optional: false,
|
||||
description: "The primary key field names used in the data models of the module.",
|
||||
},
|
||||
{
|
||||
name: "alias",
|
||||
type: "`object[]`",
|
||||
description: "The alias definitions for each data model in the module.",
|
||||
children: [
|
||||
{
|
||||
name: "name",
|
||||
type: "`string[]`",
|
||||
description: "The alias names used later when fetching or referencing the data in the model."
|
||||
},
|
||||
{
|
||||
name: "args",
|
||||
type: "`object`",
|
||||
description: "The alias's arguments.",
|
||||
children: [
|
||||
{
|
||||
name: "entity",
|
||||
type: "string",
|
||||
description: "The name of the data model this alias is defined for."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]} sectionTitle="Define __joinerConfig Method" />
|
||||
|
||||
Then, import that file in `src/modules/hello/service.ts` and add a new `__joinerConfig` method:
|
||||
|
||||
```ts title="src/modules/hello/service.ts" highlights={[["3"], ["14"], ["15"], ["16"]]}
|
||||
// other imports...
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import joinerConfig from "./joiner-config"
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory<
|
||||
// ...
|
||||
>(
|
||||
// ...
|
||||
) {
|
||||
|
||||
// ...
|
||||
|
||||
__joinerConfig(): ModuleJoinerConfig {
|
||||
return joinerConfig
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Querying a Queryable Module
|
||||
|
||||
The next chapter explains how to query the data of a Queryable module.
|
||||
@@ -0,0 +1,392 @@
|
||||
import { TypeList, Tabs, TabsList, TabsTrigger, TabsContent, TabsContentWrapper } from "docs-ui"
|
||||
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Remote Query`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this chapter, you’ll learn about the remote query and how to use it to fetch data from modules.
|
||||
|
||||
## What is the Remote Query?
|
||||
|
||||
The remote query fetches data across queryable modules. It’s a function registered in the Medusa container under the `remoteQuery` key.
|
||||
|
||||
In your resources, such as API routes or workflows, you can resolve the remote query to fetch data across custom modules and Medusa’s commerce modules.
|
||||
|
||||
---
|
||||
|
||||
## Example: Query Hello Module
|
||||
|
||||
For example, create the route `src/api/store/query/route.ts` with the following content:
|
||||
|
||||
export const exampleHighlights = [
|
||||
["18", "", "Resolve the remote query from the Medusa container."],
|
||||
["21", "remoteQueryObjectFromString", "Utility function to build the query."],
|
||||
["22", "entryPoint", "The alias name of the model you’re querying."],
|
||||
["23", "fields", "An array of the data model’s field names to retrieve in the result."],
|
||||
["27", "remoteQuery", "Run the query using the remote query."]
|
||||
]
|
||||
|
||||
```ts title="src/api/store/query/route.ts" highlights={exampleHighlights}
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
remoteQueryObjectFromString,
|
||||
ContainerRegistrationKeys,
|
||||
} from "@medusajs/utils"
|
||||
import type {
|
||||
RemoteQueryFunction,
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const remoteQuery: RemoteQueryFunction = req.scope.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_QUERY
|
||||
)
|
||||
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "my_custom",
|
||||
fields: ["name", "id"],
|
||||
})
|
||||
|
||||
res.json({
|
||||
my_customs: await remoteQuery(query),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, you resolve `remoteQuery` from the Medusa container.
|
||||
|
||||
Then, you create a query using the `remoteQueryObjectFromString` utility function imported from `@medusajs/utils`. This function accepts as a parameter an object with the following required properties:
|
||||
|
||||
- `entryPoint`: The alias name of the model you’re querying. In the previous chapter, you added to the joiner configurations of the module the alias `my_custom` for the `MyCustom` data model.
|
||||
- `fields`: An array of the data model’s field names to retrieve in the result.
|
||||
|
||||
You then pass the query to the `remoteQuery` function to retrieve the results.
|
||||
|
||||
### Test API Route
|
||||
|
||||
To test out the API route, run the Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then, send a `GET` request to the `/store/query` API route:
|
||||
|
||||
```bash apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/query"
|
||||
curl http://localhost:9000/store/query
|
||||
```
|
||||
|
||||
You’ll receive an array of `MyCustom` records under the `my_customs` key in the JSON response.
|
||||
|
||||
---
|
||||
|
||||
## Apply Filters
|
||||
|
||||
The `remoteQueryObjectFromString` function accepts a `variable` property. You can use this property to filter retrieved records.
|
||||
|
||||
For example:
|
||||
|
||||
```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "my_custom",
|
||||
fields: ["name", "id"],
|
||||
variables: {
|
||||
filters: {
|
||||
id: "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const result = await remoteQuery(query)
|
||||
```
|
||||
|
||||
The `variable` property’s value is an object having the property `filters`, whose value is also an object. The object’s properties are the field names, and the values are the filters to apply.
|
||||
|
||||
You can also filter by multiple values using array values. For example:
|
||||
|
||||
```ts highlights={[["6"], ["7"], ["8"], ["9"]]}
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "my_custom",
|
||||
fields: ["name", "id"],
|
||||
variables: {
|
||||
filters: {
|
||||
id: [
|
||||
"mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
|
||||
"mc_01HWSVWK3KYHKQEE6QGS2JC3FX",
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const result = await remoteQuery(query)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sort Records
|
||||
|
||||
To sort returned records, pass an `order` property to the `variables` property's value. The `order` property is an object whose keys are field names, and values are either:
|
||||
|
||||
- `ASC` to sort records by that field in ascending order.
|
||||
- `DESC` to sort records by that field in descending order.
|
||||
|
||||
For example:
|
||||
|
||||
```ts highlights={[["4"], ["5"], ["6"]]}
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "my_custom",
|
||||
variables: {
|
||||
order: {
|
||||
name: "DESC",
|
||||
},
|
||||
},
|
||||
fields: ["name", "id"],
|
||||
})
|
||||
|
||||
const result = await remoteQuery(query)
|
||||
```
|
||||
|
||||
This retrieves the `MyCustom` records sorted by their name in descending order.
|
||||
|
||||
---
|
||||
|
||||
## Apply Pagination
|
||||
|
||||
To paginate the returned records, pass the following properties to the `variables` property's value:
|
||||
|
||||
- `skip`: (required to apply pagination) The number of records to skip before fetching the results.
|
||||
- `take`: The number of records to fetch.
|
||||
|
||||
For example:
|
||||
|
||||
```ts highlights={[["4", "", "The number of records to skip before fetching the results."], ["5", "", "The number of records to fetch."]]}
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "my_custom",
|
||||
variables: {
|
||||
skip: 0,
|
||||
take: 10,
|
||||
},
|
||||
fields: ["name", "id"],
|
||||
})
|
||||
|
||||
const {
|
||||
rows,
|
||||
metadata: { count, take, skip },
|
||||
} = await remoteQuery(query)
|
||||
```
|
||||
|
||||
This skips no records and returns the first `10` records.
|
||||
|
||||
When the pagination fields are provided, the `remoteQuery` returns an object having two properties:
|
||||
|
||||
<TypeList types={[
|
||||
{
|
||||
name: "rows",
|
||||
type: "`array`",
|
||||
description: "The returned records."
|
||||
},
|
||||
{
|
||||
name: "metadata",
|
||||
type: "`object`",
|
||||
description: "The pagination details",
|
||||
children: [
|
||||
{
|
||||
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" />
|
||||
|
||||
---
|
||||
|
||||
## Using GraphQL
|
||||
|
||||
The remote query function alternatively accepts a string with GraphQL syntax as the query.
|
||||
|
||||
For example:
|
||||
|
||||
```ts title="src/api/store/query/route.ts" apiTesting testApiMethod="GET" testApiUrl="http://localhost:9000/store/query"
|
||||
import {
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/medusa"
|
||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
import type {
|
||||
RemoteQueryFunction,
|
||||
} from "@medusajs/modules-sdk"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const remoteQuery: RemoteQueryFunction = req.scope.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_QUERY
|
||||
)
|
||||
|
||||
const query = `
|
||||
query {
|
||||
my_custom {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
res.json({
|
||||
my_customs: await remoteQuery(query),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This runs a GraphQL query to retrieve `MyCustom` records.
|
||||
|
||||
### Example Usages
|
||||
|
||||
<Tabs defaultValue="filters" layoutType="vertical" className="mt-2">
|
||||
<TabsList>
|
||||
<TabsTrigger value="filters">Apply Filters</TabsTrigger>
|
||||
<TabsTrigger value="sort">Sort Records</TabsTrigger>
|
||||
<TabsTrigger value="pagination">Apply Pagination</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContentWrapper>
|
||||
<TabsContent value="filters" className="[&_h3]:!mt-0">
|
||||
|
||||
### Apply Filters with GraphQL
|
||||
|
||||
The `remoteQuery` function accepts as a second parameter an object of variables to reference in the GraphQL query.
|
||||
|
||||
For example, to filter the items by their ID:
|
||||
|
||||
```ts highlights={[["2"], ["3"], ["12"], ["13"], ["14"]]}
|
||||
const query = `
|
||||
query($id: ID) {
|
||||
my_custom(id: $id) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const result = await remoteQuery(
|
||||
query,
|
||||
{
|
||||
id: "mc_01HWSVWK3KYHKQEE6QGS2JC3FX"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The variable’s value can also be an array to match multiple items.
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="sort" className="[&_h3]:!mt-0">
|
||||
|
||||
### Sort Records with GraphQL
|
||||
|
||||
To sort the records by a field, pass an `order` argument whose value is an object. The object’s key is the field’s name, and its value is either:
|
||||
|
||||
- `ASC` to sort items by that field in ascending order.
|
||||
- `DESC` to sort items by that field in descending order.
|
||||
|
||||
For example:
|
||||
|
||||
```ts highlights={[["3"]]}
|
||||
const query = `
|
||||
query {
|
||||
my_custom(order: {name: DESC}) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const result = await remoteQuery(query)
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="pagination" className="[&_h3]:!mt-0">
|
||||
|
||||
### Pagination with GraphQL
|
||||
|
||||
To paginate the records retrieved, pass a `skip` and `take` records in your query, and pass their values in the second parameter of the `remoteQuery` function.
|
||||
|
||||
For example:
|
||||
|
||||
```ts highlights={[["2"], ["3"]]}
|
||||
const query = `
|
||||
query($skip: Int, $take: Int) {
|
||||
my_custom(skip: $skip, take: $take) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const {
|
||||
rows,
|
||||
metadata: { count, take, skip }
|
||||
} = await remoteQuery(
|
||||
query,
|
||||
{
|
||||
skip: 0,
|
||||
take: 10
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This skips no records and returns the first `10` records.
|
||||
|
||||
When the pagination fields are provided, the `remoteQuery` returns an object having two properties:
|
||||
|
||||
<TypeList types={[
|
||||
{
|
||||
name: "rows",
|
||||
type: "`array`",
|
||||
description: "The returned records."
|
||||
},
|
||||
{
|
||||
name: "metadata",
|
||||
type: "`object`",
|
||||
description: "The pagination details",
|
||||
children: [
|
||||
{
|
||||
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="Pagination with GraphQL" />
|
||||
|
||||
</TabsContent>
|
||||
</TabsContentWrapper>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,160 @@
|
||||
export const metadata = {
|
||||
title: `${pageNumber} Service Factory`,
|
||||
}
|
||||
|
||||
# {metadata.title}
|
||||
|
||||
In this document, you’ll learn about what the service factory is and how to use it to create a service.
|
||||
|
||||
## What is the Service Factory?
|
||||
|
||||
Medusa provides a service factory that your module’s services can extend. The service factory implements data management methods for the data models you specify.
|
||||
|
||||
If your service provides functionalities to manage the records of a data model, it’s encouraged to extend the service factory.
|
||||
|
||||
---
|
||||
|
||||
## How to Extend the Service Factory?
|
||||
|
||||
<Note type="check">
|
||||
|
||||
Your module must be using the following loaders from the previous chapters:
|
||||
|
||||
- [Container Loader](../container/page.mdx).
|
||||
- [Connection Loader](../connection-loader/page.mdx).
|
||||
|
||||
</Note>
|
||||
|
||||
The `ModulesSdkUtils` imported from `@medusajs/utils` has a utility function `abstractModuleServiceFactory` that creates a service instance with basic data management methods.
|
||||
|
||||
So, to create a service that extends the service instance returned by the factory, call `abstractModuleServiceFactory` and extend its returned value:
|
||||
|
||||
```ts title="src/modules/hello/service.ts"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory(
|
||||
MyCustom, []
|
||||
) {
|
||||
// TODO implement custom methods
|
||||
}
|
||||
|
||||
export default HelloModuleService
|
||||
```
|
||||
|
||||
### Function Parameters
|
||||
|
||||
The `abstractModuleServiceFactory` function accepts two parameters:
|
||||
|
||||
1. The first parameter is the main data model this service is creating methods for. It's the `MyCustom` in the example above.
|
||||
2. The second parameter is an array of data models to generate methods for. If you have the `AnotherCustom` data model, this is where you add it.
|
||||
|
||||
### Generated Methods
|
||||
|
||||
The service factory generates the following methods for the main data model:
|
||||
|
||||
- `list`: Returns an array of records based on filters and pagination configurations.
|
||||
- `listAndCount`: Returns a tuple of an array of records and the total count of available records based on the filters and pagination configurations provided.
|
||||
- `retrieve`: Returns a record by its ID.
|
||||
- `delete`: Deletes records by an ID or filter.
|
||||
- `softDelete`: Soft-deletes a record by an ID or filter. This only applies if the data model has a `deleted_at` field.
|
||||
- `restore`: Restores a soft-deleted record by an ID or filter. This only applies if the data model has a `deleted_at` field.
|
||||
|
||||
For other provided data models, the same methods are generated, with the data model’s name appended to the end of the method. For example, `listAnotherCustom` or `retrieveAnotherCustom`.
|
||||
|
||||
### Type Arguments
|
||||
|
||||
For a better development experience and accurate typing of the generated methods, the `abstractModuleServiceFactory` function accepts three type arguments:
|
||||
|
||||
export const typeArgsHighlights = [
|
||||
["24", "InjectedDependencies", "The type of dependencies resolved from the Module's container."],
|
||||
["25", "MyCustomDTO", "The expected input/output type of the main data model's generated methods."],
|
||||
["26", "AllModelsDTO", "The expected input/output type of the generated methods of every data model."],
|
||||
]
|
||||
|
||||
```ts title="src/modules/hello/service.ts" highlights={typeArgsHighlights}
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { MyCustom } from "./models/my-custom"
|
||||
|
||||
type MyCustomDTO = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
type InjectedDependencies = {
|
||||
// TODO add dependencies
|
||||
}
|
||||
|
||||
type AllModelsDTO = {
|
||||
MyCustom: {
|
||||
dto: MyCustomDTO
|
||||
}
|
||||
}
|
||||
|
||||
// add other data models in your module here.
|
||||
const generateMethodsFor = []
|
||||
|
||||
class HelloModuleService extends ModulesSdkUtils
|
||||
.abstractModuleServiceFactory<
|
||||
InjectedDependencies,
|
||||
MyCustomDTO,
|
||||
AllModelsDTO
|
||||
>(MyCustom, generateMethodsFor) {
|
||||
// TODO implement custom methods
|
||||
}
|
||||
|
||||
export default HelloModuleService
|
||||
```
|
||||
|
||||
1. The first one is the type of the injected dependencies received as the first parameter of the service’s constructor. You use this type to specify the resources to resolve from the module’s container.
|
||||
2. The second one is the expected input and output type of the main data model’s methods (for example, the `list` method). This is useful to infer the correct input/output of the generated methods of the main model.
|
||||
3. The third type is the expected input and output type of all data models that the service factory generates methods for (for example, the `listAnotherCustom` method). This is useful to infer the generated methods for each specified data model, with the correct input/output for each of them.
|
||||
|
||||
---
|
||||
|
||||
## Test it Out
|
||||
|
||||
To test out the service, create an API route at `src/api/store/custom/route.ts` with the following content:
|
||||
|
||||
```ts title="src/api/store/custom/route.ts"
|
||||
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
|
||||
import HelloModuleService from "../../../modules/hello/service"
|
||||
|
||||
export async function GET(
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse
|
||||
): Promise<void> {
|
||||
const helloModuleService: HelloModuleService =
|
||||
req.scope.resolve(
|
||||
"helloModuleService"
|
||||
)
|
||||
|
||||
res.json({
|
||||
my_customs: await helloModuleService.list(),
|
||||
})
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Then, start your Medusa application:
|
||||
|
||||
```bash npm2yarn
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Finally, send a request `GET` request to `/store/custom`:
|
||||
|
||||
```bash apiTesting testApiUrl="http://localhost:9000/store/custom" testApiMethod="GET"
|
||||
curl http://localhost:9000/store/custom
|
||||
```
|
||||
|
||||
You’ll receive the following response:
|
||||
|
||||
```json
|
||||
{
|
||||
"my_customs": []
|
||||
}
|
||||
```
|
||||
|
||||
In the next chapter, you’ll learn how to add create and update methods to the service generated by the service factory.
|
||||
Reference in new issue
Block a user