--- sidebar_label: "Extend Cart" tags: - cart - tutorial - extend module - server --- import { Prerequisites } from "docs-ui" export const metadata = { title: `Extend Cart Data Model`, } # {metadata.title} In this documentation, you'll learn how to extend a data model of the Cart 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 [Cart data model](/references/cart/models/Cart) defined in the Cart Module. You'll then learn how to: - Link the `Custom` data model to the `Cart` data model. - Set the `custom_name` property when a cart is created or updated using Medusa's API routes. - Retrieve the `custom_name` property with the cart's details, in custom or existing API routes. ## 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 `Cart` 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 Cart Data Model Next, you'll define a module link between the `Custom` and `Cart` 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/cart-custom.ts` with the following content: ```ts title="src/links/cart-custom.ts" import { defineLink } from "@medusajs/framework/utils" import HelloModule from "../modules/hello" import CartModule from "@medusajs/medusa/cart" export default defineLink( CartModule.linkable.cart, HelloModule.linkable.custom ) ``` This defines a link between the `Cart` 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 `Cart` 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 cartCreated Workflow Hook When a cart is created, you also want to create a `Custom` record and set the `custom_name` property, then create a link between the `Cart` and `Custom` records. To do that, you'll consume the [cartCreated](/references/medusa-workflows/createCartWorkflow#cartcreated) hook of the [createCartWorkflow](/references/medusa-workflows/createCartWorkflow). This workflow is executed in the [Create Cart API Route](!api!/store#carts_postcarts). 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: "/store/carts", 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 cart. Start by creating the step that creates the `Custom` record. Create the file `src/workflows/create-custom-from-cart/steps/create-custom.ts` with the following content: ```ts title="src/workflows/create-custom-from-cart/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-cart/index.ts` with the following content: ```ts title="src/workflows/create-custom-from-cart/index.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports" import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk" import { CartDTO } from "@medusajs/framework/types" import { createCustomStep } from "./steps/create-custom" import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" import { Modules } from "@medusajs/framework/utils" import { HELLO_MODULE } from "../../modules/hello" export type CreateCustomFromCartWorkflowInput = { cart: CartDTO additional_data?: { custom_name?: string } } export const createCustomFromCartWorkflow = createWorkflow( "create-custom-from-cart", (input: CreateCustomFromCartWorkflowInput) => { 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.CART]: { cart_id: input.cart.id, }, [HELLO_MODULE]: { custom_id: custom.id, }, }]) }) return new WorkflowResponse({ custom, }) } ) ``` The workflow accepts as an input the created cart and the `additional_data` parameter passed in the request. This is the same input that the `cartCreated` 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 cart 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 call the workflow in the hook handler. ### Consume Workflow Hook You can now consume the `cartCreated` hook, which is executed in the `createCartWorkflow` after the cart is created. To consume the hook, create the file `src/workflow/hooks/cart-created.ts` with the following content: ```ts title="src/workflow/hooks/cart-created.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" import { createCartWorkflow } from "@medusajs/medusa/core-flows" import { createCustomFromCartWorkflow, CreateCustomFromCartWorkflowInput, } from "../create-custom-from-cart" createCartWorkflow.hooks.cartCreated( async (hookData, { container }) => { await createCustomFromCartWorkflow(container) .run({ input: hookData as CreateCustomFromCartWorkflowInput, }) } ) ``` The hook handler executes the `createCustomFromCartWorkflow`, passing it its input. ### Test it Out To test it out, send a `POST` request to `/store/carts` to create a cart, passing `custom_name` in `additional_data`: ```bash curl -X POST 'localhost:9000/store/carts' \ -H 'x-publishable-api-key: {publishable_api_key}' \ -H 'Content-Type: application/json' \ --data '{ "region_id": "reg_01J9NNNWVV0T71PT44EAMTJCMP", "additional_data": { "custom_name": "test" } }' ``` Make sure to replace `{publishable_api_key}` with your publishable API key, which you can retrieve from the Medusa Admin. Also, replace the value of `region_id` with an ID of a region in your application. The request will return the cart's details. You'll learn how to retrieve the `custom_name` property with the cart's details in the next section. --- ## Step 5: Retrieve custom_name with Cart 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 To retrieve the `custom_name` property when you're retrieving the cart through API routes, such as the [Get Cart API Route](!api!/store#carts_getcartsid), pass in the `fields` query parameter `+custom.*`, which retrieves the linked `Custom` record's details. The `+` prefix in `+custom.*` indicates that the relation should be retrieved with the default cart fields. Learn more about selecting fields and relations in the [API reference](!api!/store#select-fields-and-relations). For example: ```bash curl -X POST 'localhost:9000/store/carts/{cart_id}?fields=+custom.*' \ -H 'x-publishable-api-key: {publishable_api_key}' ``` Make sure to replace `{cart_id}` with the cart's ID, and `{publishable_api_key}` with your publishable API key, which you can retrieve from the Medusa Admin. Among the returned `cart` object, you'll find a `custom` property which holds the details of the linked `Custom` record: ```json { "cart": { // ... "custom": { "id": "01J9NP7ANXDZ0EAYF0956ZE1ZA", "custom_name": "test", "created_at": "2024-10-08T09:09:06.877Z", "updated_at": "2024-10-08T09:09:06.877Z", "deleted_at": null } } } ``` ### Retrieve using Query You can also retrieve the `Custom` record linked to a cart in your code using [Query](!docs!/learn/fundamentals/module-links/query). For example: ```ts const { data: [cart] } = await query.graph({ entity: "cart", fields: ["*", "custom.*"], filters: { id: cart_id, }, }) ``` Learn more about how to use Query in [this guide](!docs!/learn/fundamentals/module-links/query). --- ## Step 6: Consume cartUpdated Workflow Hook Similar to the `cartCreated` hook, you'll consume the [cartUpdated](/references/medusa-workflows/updateCartWorkflow#cartupdated) hook of the [updateCartWorkflow](/references/medusa-workflows/updateCartWorkflow) to update `custom_name` when the cart is updated. The `updateCartWorkflow` is executed by the [Update Cart API route](!api!/store#carts_postcartsid), 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 cart 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: "/store/carts/:id", additionalDataValidator: { custom_name: z.string().nullish(), }, }, ], }) ``` The validation schema is similar to that of the Create Cart 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 cart is deleted. 2. If `additional_data.custom_name` is set and the cart doesn't have a linked `Custom` record, a new record is created and linked to the cart. 3. If `additional_data.custom_name` is set and the cart 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-cart/steps/update-custom.ts` with the following content: ```ts title="src/workflows/update-custom-from-cart/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-cart/steps/delete-custom.ts` with the following content: ```ts title="src/workflows/update-custom-from-cart/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-cart/index.ts` with the following content: ```ts title="src/workflows/update-custom-from-cart/index.ts" collapsibleLines="1-9" expandButtonLabel="Show Imports" import { CartDTO } 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-cart/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 UpdateCustomFromCartStepInput = { cart: CartDTO additional_data?: { custom_name?: string | null } } export const updateCustomFromCartWorkflow = createWorkflow( "update-custom-from-cart", (input: UpdateCustomFromCartStepInput) => { const { data: carts } = useQueryGraphStep({ entity: "cart", fields: ["custom.*"], filters: { id: input.cart.id, }, }) // TODO create, update, or delete Custom record } ) ``` The workflow accepts the same input as the `cartUpdated` workflow hook handler would. In the workflow, you retrieve the cart's linked `Custom` record using Query. Next, replace the `TODO` with the following: ```ts title="src/workflows/update-custom-from-cart/index.ts" const created = when( "create-cart-custom-link", { input, carts, }, (data) => !data.carts[0].custom && data.input.additional_data?.custom_name?.length > 0 ) .then(() => { const custom = createCustomStep({ custom_name: input.additional_data.custom_name, }) createRemoteLinkStep([{ [Modules.CART]: { cart_id: input.cart.id, }, [HELLO_MODULE]: { custom_id: custom.id, }, }]) return custom }) // TODO update, or delete Custom record ``` Using `when-then`, you check if the cart 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 cart. 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-cart/index.ts" const deleted = when( "delete-cart-custom-link", { input, carts, }, (data) => data.carts[0].custom && ( data.input.additional_data?.custom_name === null || data.input.additional_data?.custom_name.length === 0 ) ) .then(() => { deleteCustomStep({ custom: carts[0].custom, }) dismissRemoteLinkStep({ [HELLO_MODULE]: { custom_id: carts[0].custom.id, }, }) return carts[0].custom.id }) // TODO delete Custom record ``` Using `when-then`, you check if the cart 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-cart/index.ts" const updated = when({ input, carts, }, (data) => data.carts[0].custom && data.input.additional_data?.custom_name?.length > 0) .then(() => { return updateCustomStep({ id: carts[0].custom.id, custom_name: input.additional_data.custom_name, }) }) return new WorkflowResponse({ created, updated, deleted, }) ``` Using `when-then`, you check if the cart 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 cartUpdated Workflow Hook You can now consume the `cartUpdated` and execute the workflow you created. Create the file `src/workflows/hooks/cart-updated.ts` with the following content: ```ts title="src/workflows/hooks/cart-updated.ts" import { updateCartWorkflow } from "@medusajs/medusa/core-flows" import { UpdateCustomFromCartStepInput, updateCustomFromCartWorkflow, } from "../update-custom-from-cart" updateCartWorkflow.hooks.cartUpdated( async (hookData, { container }) => { await updateCustomFromCartWorkflow(container) .run({ input: hookData as UpdateCustomFromCartStepInput, }) } ) ``` 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 `/store/carts/:id` to update a cart, passing `custom_name` in `additional_data`: ```bash curl -X POST 'localhost:9000/store/carts/{cart_id}?fields=+custom.*' \ -H 'x-publishable-api-key: {publishable_api_key}' \ -H 'Content-Type: application/json' \ --data '{ "additional_data": { "custom_name": "test 2" } }' ``` Make sure to replace `{cart_id}` with the cart's ID, and `{publishable_api_key}` with your publishable API key, which you can retrieve from the Medusa Admin. The request will return the cart's details with the updated `custom` linked record.