Extend Cart Data Model

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 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.

TipIf you don't have a module, follow this guide 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:

src/modules/hello/models/custom.ts
1import { model } from "@medusajs/framework/utils"2
3export const Custom = model.define("custom", {4  id: model.id().primaryKey(),5  custom_name: model.text(),6})

This creates a Custom data model that has the id and custom_name properties.

TipLearn more about data models in this guide.

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.

TipLearn more about module links in this guide.

Create the file src/links/cart-custom.ts with the following content:

src/links/cart-custom.ts
1import { defineLink } from "@medusajs/framework/utils"2import HelloModule from "../modules/hello"3import CartModule from "@medusajs/medusa/cart"4
5export default defineLink(6  CartModule.linkable.cart,7  HelloModule.linkable.custom8)

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:

Terminal
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:

Terminal
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 hook of the createCartWorkflow. This workflow is executed in the Create Cart API Route.

TipLearn more about workflow hooks in this guide.

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:

src/api/middlewares.ts
1import { defineMiddlewares } from "@medusajs/framework/http"2import { z } from "zod"3
4export default defineMiddlewares({5  routes: [6    {7      method: "POST",8      matcher: "/store/carts",9      additionalDataValidator: {10        custom_name: z.string().optional(),11      },12    },13  ],14})

The additional_data parameter validation is customized using defineMiddlewares. In the routes middleware configuration object, the additionalDataValidator property accepts Zod 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.

TipLearn more about additional data validation in this guide.

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:

src/workflows/create-custom-from-cart/steps/create-custom.ts
1import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"2import HelloModuleService from "../../../modules/hello/service"3import { HELLO_MODULE } from "../../../modules/hello"4
5type CreateCustomStepInput = {6  custom_name?: string7}8
9export const createCustomStep = createStep(10  "create-custom",11  async (data: CreateCustomStepInput, { container }) => {12    if (!data.custom_name) {13      return14    }15
16    const helloModuleService: HelloModuleService = container.resolve(17      HELLO_MODULE18    )19
20    const custom = await helloModuleService.createCustoms(data)21
22    return new StepResponse(custom, custom)23  },24  async (custom, { container }) => {25    const helloModuleService: HelloModuleService = container.resolve(26      HELLO_MODULE27    )28
29    await helloModuleService.deleteCustoms(custom.id)30  }31)

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.

TipLearn more about compensation functions in this guide.

Then, create the workflow at src/workflows/create-custom-from-cart/index.ts with the following content:

src/workflows/create-custom-from-cart/index.ts
6import { HELLO_MODULE } from "../../modules/hello"7
8export type CreateCustomFromCartWorkflowInput = {9  cart: CartDTO10  additional_data?: {11    custom_name?: string12  }13}14
15export const createCustomFromCartWorkflow = createWorkflow(16  "create-custom-from-cart",17  (input: CreateCustomFromCartWorkflowInput) => {18    const customName = transform(19      {20        input,21      },22      (data) => data.input.additional_data?.custom_name || ""23    )24
25    const custom = createCustomStep({26      custom_name: customName,27    })28
29    when(({ custom }), ({ custom }) => custom !== undefined)30      .then(() => {31        createRemoteLinkStep([{32          [Modules.CART]: {33            cart_id: input.cart.id,34          },35          [HELLO_MODULE]: {36            custom_id: custom.id,37          },38        }])39      })40
41    return new WorkflowResponse({42      custom,43    })44  }45)

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.
  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.

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:

src/workflow/hooks/cart-created.ts
5} from "../create-custom-from-cart"6
7createCartWorkflow.hooks.cartCreated(8	async (hookData, { container }) => {9    await createCustomFromCartWorkflow(container)10      .run({11        input: hookData as CreateCustomFromCartWorkflowInput,12      })13	}14)

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:

Code
1curl -X POST 'localhost:9000/store/carts' \2-H 'x-publishable-api-key: {publishable_api_key}' \3-H 'Content-Type: application/json' \4--data '{5    "region_id": "reg_01J9NNNWVV0T71PT44EAMTJCMP",6    "additional_data": {7        "custom_name": "test"8    }9}'

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, pass in the fields query parameter +custom.*, which retrieves the linked Custom record's details.

TipThe + 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.

For example:

Code
1curl -X POST 'localhost:9000/store/carts/{cart_id}?fields=+custom.*' \2-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:

Code
1{2  "cart": {3    // ...4    "custom": {5      "id": "01J9NP7ANXDZ0EAYF0956ZE1ZA",6      "custom_name": "test",7      "created_at": "2024-10-08T09:09:06.877Z",8      "updated_at": "2024-10-08T09:09:06.877Z",9      "deleted_at": null10    }11  }12}

Retrieve using Query#

You can also retrieve the Custom record linked to a cart in your code using Query.

For example:

Code
1const { data: [cart] } = await query.graph({2  entity: "cart",3  fields: ["*", "custom.*"],4  filters: {5    id: cart_id,6  },7})

Learn more about how to use Query in this guide.


Step 6: Consume cartUpdated Workflow Hook#

Similar to the cartCreated hook, you'll consume the cartUpdated hook of the updateCartWorkflow to update custom_name when the cart is updated.

The updateCartWorkflow is executed by the Update Cart API route, 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:

src/api/middlewares.ts
1import { defineMiddlewares } from "@medusajs/framework/http"2import { z } from "zod"3
4export default defineMiddlewares({5  routes: [6    // ...7    {8      method: "POST",9      matcher: "/store/carts/:id",10      additionalDataValidator: {11        custom_name: z.string().nullish(),12      },13    },14  ],15})

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:

src/workflows/update-custom-from-cart/steps/update-custom.ts
1import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"2import { HELLO_MODULE } from "../../../modules/hello"3import HelloModuleService from "../../../modules/hello/service"4
5type UpdateCustomStepInput = {6  id: string7  custom_name: string8}9
10export const updateCustomStep = createStep(11  "update-custom",12  async ({ id, custom_name }: UpdateCustomStepInput, { container }) => {13    const helloModuleService: HelloModuleService = container.resolve(14      HELLO_MODULE15    )16
17    const prevData = await helloModuleService.retrieveCustom(id)18
19    const custom = await helloModuleService.updateCustoms({20      id,21      custom_name,22    })23
24    return new StepResponse(custom, prevData)25  },26  async (prevData, { container }) => {27    const helloModuleService: HelloModuleService = container.resolve(28      HELLO_MODULE29    )30
31    await helloModuleService.updateCustoms(prevData)32  }33)

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:

src/workflows/update-custom-from-cart/steps/delete-custom.ts
5import { HELLO_MODULE } from "../../../modules/hello"6
7type DeleteCustomStepInput = {8  custom: InferTypeOf<typeof Custom>9}10
11export const deleteCustomStep = createStep(12  "delete-custom",13  async ({ custom }: DeleteCustomStepInput, { container }) => {14    const helloModuleService: HelloModuleService = container.resolve(15      HELLO_MODULE16    )17
18    await helloModuleService.deleteCustoms(custom.id)19
20    return new StepResponse(custom, custom)21  },22  async (custom, { container }) => {23    const helloModuleService: HelloModuleService = container.resolve(24      HELLO_MODULE25    )26
27    await helloModuleService.createCustoms(custom)28  }29)

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:

src/workflows/update-custom-from-cart/index.ts
8import { updateCustomStep } from "./steps/update-custom"9
10export type UpdateCustomFromCartStepInput = {11  cart: CartDTO12  additional_data?: {13    custom_name?: string | null14  }15}16
17export const updateCustomFromCartWorkflow = createWorkflow(18  "update-custom-from-cart",19  (input: UpdateCustomFromCartStepInput) => {20    const { data: carts } = useQueryGraphStep({21      entity: "cart",22      fields: ["custom.*"],23      filters: {24        id: input.cart.id,25      },26    })27
28    // TODO create, update, or delete Custom record29  }30)

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:

src/workflows/update-custom-from-cart/index.ts
1const created = when(2  "create-cart-custom-link",3    {4    input,5    carts,6  }, 7  (data) => 8    !data.carts[0].custom && 9    data.input.additional_data?.custom_name?.length > 010)11.then(() => {12  const custom = createCustomStep({13    custom_name: input.additional_data.custom_name,14  })15
16  createRemoteLinkStep([{17    [Modules.CART]: {18      cart_id: input.cart.id,19    },20    [HELLO_MODULE]: {21      custom_id: custom.id,22    },23  }])24
25  return custom26})27
28// 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:

src/workflows/update-custom-from-cart/index.ts
1const deleted = when(2  "delete-cart-custom-link",3  {4    input,5    carts,6  }, (data) => 7    data.carts[0].custom && (8      data.input.additional_data?.custom_name === null || 9      data.input.additional_data?.custom_name.length === 010    )11)12.then(() => {13  deleteCustomStep({14    custom: carts[0].custom,15  })16
17  dismissRemoteLinkStep({18    [HELLO_MODULE]: {19      custom_id: carts[0].custom.id,20    },21  })22
23  return carts[0].custom.id24})25
26// 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:

src/workflows/update-custom-from-cart/index.ts
1const updated = when({2  input,3  carts,4}, (data) => data.carts[0].custom && data.input.additional_data?.custom_name?.length > 0)5.then(() => {6  return updateCustomStep({7    id: carts[0].custom.id,8    custom_name: input.additional_data.custom_name,9  })10})11
12return new WorkflowResponse({13  created,14  updated,15  deleted,16})

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:

src/workflows/hooks/cart-updated.ts
1import { updateCartWorkflow } from "@medusajs/medusa/core-flows"2import { 3  UpdateCustomFromCartStepInput, 4  updateCustomFromCartWorkflow,5} from "../update-custom-from-cart"6
7updateCartWorkflow.hooks.cartUpdated(8  async (hookData, { container }) => {9    await updateCustomFromCartWorkflow(container)10      .run({11        input: hookData as UpdateCustomFromCartStepInput,12      })13  }14)

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:

Code
1curl -X POST 'localhost:9000/store/carts/{cart_id}?fields=+custom.*' \2-H 'x-publishable-api-key: {publishable_api_key}' \3-H 'Content-Type: application/json' \4--data '{5    "additional_data": {6        "custom_name": "test 2"7    }8}'

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.

Was this page helpful?
Edit this page
Ask Anything
FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break