Files
medusa-store/www/apps/book/app/advanced-development/modules/example-module-relationship/page.mdx
Shahed Nasser 4fe28f5a95 chore: reorganize docs apps (#7228)
* reorganize docs apps

* add README

* fix directory

* add condition for old docs
2024-05-03 17:36:38 +03:00

431 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
```
Youll 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 modules data models references the `id` field of the `Product` data model.
---
## Implement Create API Route
In this section, youll 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"
}'
```
Youll 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"
}
}
}
```