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.
The example in this chapter uses the same `hello` module with the `HelloModuleService` from previous chapters.
## 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
}
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
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 {
const customProductData = await this.customProductDataService_.create(
data,
context
)
return this.baseRepository_.serialize(
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.
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.
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,
res: MedusaResponse
): Promise {
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"
}
}
}
```