431 lines
12 KiB
Plaintext
431 lines
12 KiB
Plaintext
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"
|
||
}
|
||
}
|
||
}
|
||
``` |