chore: reorganize docs apps (#7228)
* reorganize docs apps * add README * fix directory * add condition for old docs
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user