www/apps/resources/app/commerce-modules/customer/extend/page.mdx
import { Prerequisites } from "docs-ui"
export const metadata = {
title: Extend Customer Data Model,
}
In this documentation, you'll learn how to extend a data model of the Customer Module to add a custom property.
You'll create a Custom data model in a module. This data model will have a custom_name property, which is the property you want to add to the Customer data model defined in the Customer Module.
You'll then learn how to:
Custom data model to the Customer data model.custom_name property when a customer is created or updated using Medusa's API routes.custom_name property with the customer's details, in custom or existing API routes.Similar steps can be applied to the CustomerAddress data model.
Consider you have a Hello Module defined in the /src/modules/hello directory.
If you don't have a module, follow this guide to create one.
</Note>To add the custom_name property to the Customer data model, you'll create in the Hello Module a data model that has the custom_name property.
Create the file src/modules/hello/models/custom.ts with the following content:
import { model } from "@medusajs/framework/utils"
export const Custom = model.define("custom", {
id: model.id().primaryKey(),
custom_name: model.text(),
})
This creates a Custom data model that has the id and custom_name properties.
Learn more about data models in this guide.
</Note>Next, you'll define a module link between the Custom and Customer data model. A module link allows you to form a relation between two data models of separate modules while maintaining module isolation.
Learn more about module links in this guide.
</Note>Create the file src/links/customer-custom.ts with the following content:
import { defineLink } from "@medusajs/framework/utils"
import HelloModule from "../modules/hello"
import CustomerModule from "@medusajs/medusa/customer"
export default defineLink(
CustomerModule.linkable.customer,
HelloModule.linkable.custom
)
This defines a link between the Customer and Custom data models. Using this link, you'll later query data across the modules, and link records of each data model.
<Prerequisites items={[ { text: "Module must be registered in medusa-config.ts", link: "!docs!/learn/fundamentals/modules#4-add-module-to-configurations" } ]} />
To reflect the Custom data model in the database, generate a migration that defines the table to be created for it.
Run the following command in your Medusa project's root:
npx medusa db:generate helloModuleService
Where helloModuleService is your module's name.
Then, run the db:migrate command to run the migrations and create a table in the database for the link between the Customer and Custom data models:
npx medusa db:migrate
A table for the link is now created in the database. You can now retrieve and manage the link between records of the data models.
When a customer is created, you also want to create a Custom record and set the custom_name property, then create a link between the Customer and Custom records.
To do that, you'll consume the customersCreated hook of the createCustomersWorkflow. This workflow is executed in the Create Customer Admin API route
<Note title="Tip">Learn more about workflow hooks in this guide.
</Note>The API route accepts in its request body an additional_data parameter. You can pass in it custom data, which is passed to the workflow hook handler.
To pass the custom_name in the additional_data parameter, you must add a validation rule that tells the Medusa application about this custom property.
Create the file src/api/middlewares.ts with the following content:
As of Medusa v2.13.0, Zod should be imported from @medusajs/framework/zod.
import { defineMiddlewares } from "@medusajs/framework/http"
import { z } from "@medusajs/framework/zod"
export default defineMiddlewares({
routes: [
{
method: "POST",
matcher: "/admin/customers",
additionalDataValidator: {
custom_name: z.string().optional(),
},
},
],
})
The additional_data parameter validation is customized using defineMiddlewares. In the routes middleware configuration object, the additionalDataValidator property accepts Zod validation rules.
In the snippet above, you add a validation rule indicating that custom_name is a string that can be passed in the additional_data object.
Learn more about additional data validation in this guide.
</Note>You'll now create a workflow that will be used in the hook handler.
This workflow will create a Custom record, then link it to the customer.
Start by creating the step that creates the Custom record. Create the file src/workflows/create-custom-from-customer/steps/create-custom.ts with the following content:
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import HelloModuleService from "../../../modules/hello/service"
import { HELLO_MODULE } from "../../../modules/hello"
type CreateCustomStepInput = {
custom_name?: string
}
export const createCustomStep = createStep(
"create-custom",
async (data: CreateCustomStepInput, { container }) => {
if (!data.custom_name) {
return
}
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
const custom = await helloModuleService.createCustoms(data)
return new StepResponse(custom, custom)
},
async (custom, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
await helloModuleService.deleteCustoms(custom.id)
}
)
In the step, you resolve the Hello Module's main service and create a Custom record.
In the compensation function that undoes the step's actions in case of an error, you delete the created record.
<Note title="Tip">Learn more about compensation functions in this guide.
</Note>Then, create the workflow at src/workflows/create-custom-from-customer/index.ts with the following content:
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { CustomerDTO } from "@medusajs/framework/types"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
import { Modules } from "@medusajs/framework/utils"
import { HELLO_MODULE } from "../../modules/hello"
import { createCustomStep } from "./steps/create-custom"
export type CreateCustomFromCustomerWorkflowInput = {
customer: CustomerDTO
additional_data?: {
custom_name?: string
}
}
export const createCustomFromCustomerWorkflow = createWorkflow(
"create-custom-from-customer",
(input: CreateCustomFromCustomerWorkflowInput) => {
const customName = transform(
{
input,
},
(data) => data.input.additional_data.custom_name || ""
)
const custom = createCustomStep({
custom_name: customName,
})
when(({ custom }), ({ custom }) => custom !== undefined)
.then(() => {
createRemoteLinkStep([{
[Modules.CUSTOMER]: {
customer_id: input.customer.id,
},
[HELLO_MODULE]: {
custom_id: custom.id,
},
}])
})
return new WorkflowResponse({
custom,
})
}
)
The workflow accepts as an input the created customer and the additional_data parameter passed in the request. This is the same input that the customersCreated hook accepts.
In the workflow, you:
transform to get the value of custom_name based on whether it's set in additional_data. Learn more about why you can't use conditional operators in a workflow without using transform in this guide.Custom record using the createCustomStep.when-then to link the customer to the Custom record if it was created. Learn more about why you can't use if-then conditions in a workflow without using when-then in this guide.You'll next execute the workflow in the hook handler.
You can now consume the customersCreated hook, which is executed in the createCustomersWorkflow after the customer is created.
To consume the hook, create the file src/workflow/hooks/user-created.ts with the following content:
import { createCustomersWorkflow } from "@medusajs/medusa/core-flows"
import {
createCustomFromCustomerWorkflow,
CreateCustomFromCustomerWorkflowInput,
} from "../create-custom-from-customer"
createCustomersWorkflow.hooks.customersCreated(
async ({ customers, additional_data }, { container }) => {
const workflow = createCustomFromCustomerWorkflow(container)
for (const customer of customers) {
await workflow.run({
input: {
customer,
additional_data,
} as CreateCustomFromCustomerWorkflowInput,
})
}
}
)
The hook handler executes the createCustomFromCustomerWorkflow, passing it its input.
To test it out, send a POST request to /admin/customers to create a customer, passing custom_name in additional_data:
curl -X POST 'localhost:9000/admin/customers' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data-raw '{
"email": "[email protected]",
"additional_data": {
"custom_name": "test"
}
}'
Make sure to replace {token} with an admin user's JWT token. Learn how to retrieve it in the API reference.
The request will return the customer's details. You'll learn how to retrieve the custom_name property with the customer's details in the next section.
When you extend an existing data model through links, you also want to retrieve the custom properties with the data model.
You can also retrieve the Custom record linked to a customer in your code using Query.
For example:
const { data: [customer] } = await query.graph({
entity: "customer",
fields: ["*", "custom.*"],
filters: {
id: customer_id,
},
})
You can use Query in a custom route to retrieve the customer with its linked Custom record.
Learn more about how to use Query in this guide.
Similar to the customersCreated hook, you'll consume the customersUpdated hook of the updateCustomersWorkflow to update custom_name when the customer is updated.
The updateCustomersWorkflow is executed by the Update Customer API route, which accepts the additional_data parameter to pass custom data to the hook.
To allow passing custom_name in the additional_data parameter of the update customer route, add in src/api/middlewares.ts a new route middleware configuration object:
As of Medusa v2.13.0, Zod should be imported from @medusajs/framework/zod.
import { defineMiddlewares } from "@medusajs/framework/http"
import { z } from "@medusajs/framework/zod"
export default defineMiddlewares({
routes: [
// ...
{
method: "POST",
matcher: "/admin/customers/:id",
additionalDataValidator: {
custom_name: z.string().nullish(),
},
},
],
})
The validation schema is the similar to that of the Create Customer API route, except you can pass a null value for custom_name to remove or unset the custom_name's value.
Next, you'll create a workflow that creates, updates, or deletes Custom records based on the provided additional_data parameter:
additional_data.custom_name is set and it's null, the Custom record linked to the customer is deleted.additional_data.custom_name is set and the customer doesn't have a linked Custom record, a new record is created and linked to the customer.additional_data.custom_name is set and the customer has a linked Custom record, the custom_name property of the Custom record is updated.Start by creating the step that updates a Custom record. Create the file src/workflows/update-custom-from-customer/steps/update-custom.ts with the following content:
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { HELLO_MODULE } from "../../../modules/hello"
import HelloModuleService from "../../../modules/hello/service"
type UpdateCustomStepInput = {
id: string
custom_name: string
}
export const updateCustomStep = createStep(
"update-custom",
async ({ id, custom_name }: UpdateCustomStepInput, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
const prevData = await helloModuleService.retrieveCustom(id)
const custom = await helloModuleService.updateCustoms({
id,
custom_name,
})
return new StepResponse(custom, prevData)
},
async (prevData, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
await helloModuleService.updateCustoms(prevData)
}
)
In this step, you update a Custom record. In the compensation function, you revert the update.
Next, you'll create the step that deletes a Custom record. Create the file src/workflows/update-custom-from-customer/steps/delete-custom.ts with the following content:
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { Custom } from "../../../modules/hello/models/custom"
import { InferTypeOf } from "@medusajs/framework/types"
import HelloModuleService from "../../../modules/hello/service"
import { HELLO_MODULE } from "../../../modules/hello"
type DeleteCustomStepInput = {
custom: InferTypeOf<typeof Custom>
}
export const deleteCustomStep = createStep(
"delete-custom",
async ({ custom }: DeleteCustomStepInput, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
await helloModuleService.deleteCustoms(custom.id)
return new StepResponse(custom, custom)
},
async (custom, { container }) => {
const helloModuleService: HelloModuleService = container.resolve(
HELLO_MODULE
)
await helloModuleService.createCustoms(custom)
}
)
In this step, you delete a Custom record. In the compensation function, you create it again.
Finally, you'll create the workflow. Create the file src/workflows/update-custom-from-customer/index.ts with the following content:
import { CustomerDTO } from "@medusajs/framework/types"
import { createWorkflow, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createRemoteLinkStep, dismissRemoteLinkStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { createCustomStep } from "../create-custom-from-customer/steps/create-custom"
import { Modules } from "@medusajs/framework/utils"
import { HELLO_MODULE } from "../../modules/hello"
import { deleteCustomStep } from "./steps/delete-custom"
import { updateCustomStep } from "./steps/update-custom"
export type UpdateCustomFromCustomerStepInput = {
customer: CustomerDTO
additional_data?: {
custom_name?: string | null
}
}
export const updateCustomFromCustomerWorkflow = createWorkflow(
"update-custom-from-customer",
(input: UpdateCustomFromCustomerStepInput) => {
const { data: customers } = useQueryGraphStep({
entity: "customer",
fields: ["custom.*"],
filters: {
id: input.customer.id,
},
})
// TODO create, update, or delete Custom record
}
)
The workflow accepts the same input as the customersUpdated workflow hook handler would.
In the workflow, you retrieve the customer's linked Custom record using Query.
Next, replace the TODO with the following:
const created = when(
"create-customer-custom-link",
{
input,
customers,
}, (data) =>
!data.customers[0].custom &&
data.input.additional_data?.custom_name?.length > 0
)
.then(() => {
const custom = createCustomStep({
custom_name: input.additional_data.custom_name,
})
createRemoteLinkStep([{
[Modules.CUSTOMER]: {
customer_id: input.customer.id,
},
[HELLO_MODULE]: {
custom_id: custom.id,
},
}])
return custom
})
// TODO update, or delete Custom record
Using when-then, you check if the customer doesn't have a linked Custom record and the custom_name property is set. If so, you create a Custom record and link it to the customer.
To create the Custom record, you use the createCustomStep you created in an earlier section.
Next, replace the new TODO with the following:
const deleted = when(
"delete-customer-custom-link",
{
input,
customers,
}, (data) =>
data.customers[0].custom && (
data.input.additional_data?.custom_name === null ||
data.input.additional_data?.custom_name.length === 0
)
)
.then(() => {
deleteCustomStep({
custom: customers[0].custom,
})
dismissRemoteLinkStep({
[HELLO_MODULE]: {
custom_id: customers[0].custom.id,
},
})
return customers[0].custom.id
})
// TODO delete Custom record
Using when-then, you check if the customer has a linked Custom record and custom_name is null or an empty string. If so, you delete the linked Custom record and dismiss its links.
Finally, replace the new TODO with the following:
const updated = when({
input,
customers,
}, (data) => data.customers[0].custom && data.input.additional_data?.custom_name?.length > 0)
.then(() => {
return updateCustomStep({
id: customers[0].custom.id,
custom_name: input.additional_data.custom_name,
})
})
return new WorkflowResponse({
created,
updated,
deleted,
})
Using when-then, you check if the customer has a linked Custom record and custom_name is passed in the additional_data. If so, you update the linked Custom record.
You return in the workflow response the created, updated, and deleted Custom record.
You can now consume the customersUpdated and execute the workflow you created.
Create the file src/workflows/hooks/customer-updated.ts with the following content:
import { updateCustomersWorkflow } from "@medusajs/medusa/core-flows"
import {
UpdateCustomFromCustomerStepInput,
updateCustomFromCustomerWorkflow,
} from "../update-custom-from-customer"
updateCustomersWorkflow.hooks.customersUpdated(
async ({ customers, additional_data }, { container }) => {
const workflow = updateCustomFromCustomerWorkflow(container)
for (const customer of customers) {
await workflow.run({
input: {
customer,
additional_data,
} as UpdateCustomFromCustomerStepInput,
})
}
}
)
In the workflow hook handler, you execute the workflow, passing it the hook's input.
To test it out, send a POST request to /admin/customers/:id to update a customer, passing custom_name in additional_data:
curl -X POST 'localhost:9000/admin/customers/{customer_id}' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
"additional_data": {
"custom_name": "test3"
}
}'
Make sure to replace {customer_id} with the customer's ID, and {token} with the JWT token of an admin user.
The request will return the customer's details with the updated custom linked record.