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" } } } ```