--- sidebar_label: "Extend Customer" tags: - customer - tutorial - extend module - server --- import { Prerequisites } from "docs-ui" export const metadata = { title: `Extend Customer Data Model`, } # {metadata.title} 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](/references/customer/models/Customer) defined in the Customer Module. You'll then learn how to: - Link the `Custom` data model to the `Customer` data model. - Set the `custom_name` property when a customer is created or updated using Medusa's API routes. - Retrieve the `custom_name` property with the customer's details, in custom or existing API routes. Similar steps can be applied to the `CustomerAddress` data model. ## Step 1: Define Custom 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](!docs!/learn/fundamentals/modules) to create one. 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: ```ts title="src/modules/hello/models/custom.ts" 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](!docs!/learn/fundamentals/modules#1-create-data-model). --- ## Step 2: Define Link to Customer Data Model 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](!docs!/learn/fundamentals/module-links). Create the file `src/links/customer-custom.ts` with the following content: ```ts title="src/links/customer-custom.ts" 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. --- ## Step 3: Generate and Run Migrations 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: ```bash 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: ```bash 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. --- ## Step 4: Consume customersCreated Workflow Hook 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](/references/medusa-workflows/createCustomersWorkflow#customerscreated) hook of the [createCustomersWorkflow](/references/medusa-workflows/createCustomersWorkflow). This workflow is executed in the [Create Customer Admin API route](!api!/admin#customers_postcustomers) Learn more about workflow hooks in [this guide](!docs!/learn/fundamentals/workflows/workflow-hooks). 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. ### Add custom_name to Additional Data Validation 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: ```ts title="src/api/middlewares.ts" import { defineMiddlewares } from "@medusajs/framework/http" import { z } from "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](https://zod.dev/) validaiton 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](!docs!/learn/fundamentals/api-routes/additional-data). ### Create Workflow to Create Custom Record 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: ```ts title="src/workflows/create-custom-from-customer/steps/create-custom.ts" 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. Learn more about compensation functions in [this guide](!docs!/learn/fundamentals/workflows/compensation-function). Then, create the workflow at `src/workflows/create-custom-from-customer/index.ts` with the following content: ```ts title="src/workflows/create-custom-from-customer/index.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports" 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: 1. Use `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](!docs!/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). 2. Create the `Custom` record using the `createCustomStep`. 3. Use `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](!docs!/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). You'll next execute the workflow in the hook handler. ### Consume Workflow Hook 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: ```ts title="src/workflow/hooks/user-created.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" 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. ### Test it Out To test it out, send a `POST` request to `/admin/customers` to create a customer, passing `custom_name` in `additional_data`: ```bash curl -X POST 'localhost:9000/admin/customers' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer {token}' \ --data-raw '{ "email": "customer@gmail.com", "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](!api!/admin#1-bearer-authorization-with-jwt-tokens). 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. --- ## Step 5: Retrieve custom_name with Customer Details When you extend an existing data model through links, you also want to retrieve the custom properties with the data model. ### Retrieve in API Routes using Query You can also retrieve the `Custom` record linked to a customer in your code using [Query](!docs!/learn/fundamentals/module-links/query). For example: ```ts 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](!docs!/learn/fundamentals/module-links/query). --- ## Step 6: Consume customersUpdated Workflow Hook Similar to the `customersCreated` hook, you'll consume the [customersUpdated](/references/medusa-workflows/updateCustomersWorkflow#customersUpdated) hook of the [updateCustomersWorkflow](/references/medusa-workflows/updateCustomersWorkflow) to update `custom_name` when the customer is updated. The `updateCustomersWorkflow` is executed by the [Update Customer API route](!api!/admin#customers_postcustomersid), which accepts the `additional_data` parameter to pass custom data to the hook. ### Add custom_name to Additional Data Validation 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: ```ts title="src/api/middlewares.ts" import { defineMiddlewares } from "@medusajs/framework/http" import { z } from "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. ### Create Workflow to Update Custom Record Next, you'll create a workflow that creates, updates, or deletes `Custom` records based on the provided `additional_data` parameter: 1. If `additional_data.custom_name` is set and it's `null`, the `Custom` record linked to the customer is deleted. 2. If `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. 3. If `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: ```ts title="src/workflows/update-custom-from-customer/steps/update-custom.ts" 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: ```ts title="src/workflows/update-custom-from-customer/steps/delete-custom.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" 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 } 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: ```ts title="src/workflows/update-custom-from-customer/index.ts" collapsibleLines="1-9" expandButtonLabel="Show Imports" 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: ```ts title="src/workflows/update-custom-from-customer/index.ts" 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: ```ts title="src/workflows/update-custom-from-customer/index.ts" 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: ```ts title="src/workflows/update-custom-from-customer/index.ts" 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. ### Consume customersUpdated Workflow Hook 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: ```ts title="src/workflows/hooks/customer-updated.ts" 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. ### Test it Out To test it out, send a `POST` request to `/admin/customers/:id` to update a customer, passing `custom_name` in `additional_data`: ```bash 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.