- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
Extend Promotion Data Model
In this documentation, you'll learn how to extend a data model of the Promotion 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 Promotion data model defined in the Promotion Module.
You'll then learn how to:
- Link the
Custom
data model to thePromotion
data model. - Set the
custom_name
property when a promotion is created or updated using Medusa's API routes. - Retrieve the
custom_name
property with the promotion's details, in custom or existing API routes.
Campaign
data model.Step 1: Define Custom Data Model#
Consider you have a Hello Module defined in the /src/modules/hello
directory.
To add the custom_name
property to the Promotion
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:
This creates a Custom
data model that has the id
and custom_name
properties.
Step 2: Define Link to Promotion Data Model#
Next, you'll define a module link between the Custom
and Promotion
data model. A module link allows you to form a relation between two data models of separate modules while maintaining module isolation.
Create the file src/links/promotion-custom.ts
with the following content:
This defines a link between the Promotion
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:
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 Promotion
and Custom
data models:
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 promotionsCreated Workflow Hook#
When a promotion is created, you also want to create a Custom
record and set the custom_name
property, then create a link between the Promotion
and Custom
records.
To do that, you'll consume the promotionsCreated hook of the createPromotionsWorkflow. This workflow is executed in the Create Promotion Admin API route
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:
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.
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 promotion.
Start by creating the step that creates the Custom
record. Create the file src/workflows/create-custom-from-promotion/steps/create-custom.ts
with the following content:
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.
Then, create the workflow at src/workflows/create-custom-from-promotion/index.ts
with the following content:
6import { createCustomStep } from "./steps/create-custom"7 8export type CreateCustomFromPromotionWorkflowInput = {9 promotion: PromotionDTO10 additional_data?: {11 custom_name?: string12 }13}14 15export const createCustomFromPromotionWorkflow = createWorkflow(16 "create-custom-from-promotion",17 (input: CreateCustomFromPromotionWorkflowInput) => {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.PROMOTION]: {33 promotion_id: input.promotion.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 promotion and the additional_data
parameter passed in the request. This is the same input that the promotionsCreated
hook accepts.
In the workflow, you:
- Use
transform
to get the value ofcustom_name
based on whether it's set inadditional_data
. Learn more about why you can't use conditional operators in a workflow without usingtransform
in this guide. - Create the
Custom
record using thecreateCustomStep
. - Use
when-then
to link the promotion to theCustom
record if it was created. Learn more about why you can't use if-then conditions in a workflow without usingwhen-then
in this guide.
You'll next execute the workflow in the hook handler.
Consume Workflow Hook#
You can now consume the promotionsCreated
hook, which is executed in the createPromotionsWorkflow
after the promotion is created.
To consume the hook, create the file src/workflow/hooks/promotion-created.ts
with the following content:
5} from "../create-custom-from-promotion"6 7createPromotionsWorkflow.hooks.promotionsCreated(8 async ({ promotions, additional_data }, { container }) => {9 const workflow = createCustomFromPromotionWorkflow(container)10 11 for (const promotion of promotions) {12 await workflow.run({13 input: {14 promotion,15 additional_data,16 } as CreateCustomFromPromotionWorkflowInput,17 })18 }19 }20)
The hook handler executes the createPromotionsWorkflow
, passing it its input.
Test it Out#
To test it out, send a POST
request to /admin/promotions
to create a promotion, passing custom_name
in additional_data
:
1curl --location 'localhost:9000/admin/promotions' \2-H 'Content-Type: application/json' \3-H 'Authorization: Bearer {token}' \4--data '{5 "additional_data": {6 "custom_name": "test"7 },8 "code": "50OFF",9 "type": "standard",10 "application_method": {11 "description": "My promotion",12 "value": 50,13 "currency_code": "usd",14 "max_quantity": 1,15 "type": "percentage",16 "target_type": "items",17 "apply_to_quantity": 0,18 "buy_rules_min_quantity": 1,19 "allocation": "each"20 }21}'
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 promotion's details. You'll learn how to retrieve the custom_name
property with the promotion's details in the next section.
Step 5: Retrieve custom_name with Promotion 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 promotion through API routes, such as the Get Promotion API Route, pass in the fields
query parameter +custom.*
, which retrieves the linked Custom
record's details.
+
prefix in +custom.*
indicates that the relation should be retrieved with the default promotion fields. Learn more about selecting fields and relations in the API reference.For example:
Make sure to replace {promotion_id}
with the promotion's ID, and {token}
with an admin user's JWT token.
Among the returned promotion
object, you'll find a custom
property which holds the details of the linked Custom
record:
Retrieve using Query#
You can also retrieve the Custom
record linked to a promotion in your code using Query.
For example:
Learn more about how to use Query in this guide.
Step 6: Consume promotionsUpdated Workflow Hook#
Similar to the promotionsCreated
hook, you'll consume the promotionsUpdated hook of the updatePromotionsWorkflow to update custom_name
when the promotion is updated.
The updatePromotionsWorkflow
is executed by the Update Promotion 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 promotion route, add in src/api/middlewares.ts
a new route middleware configuration object:
1import { defineMiddlewares } from "@medusajs/framework/http"2import { z } from "zod"3 4export default defineMiddlewares({5 routes: [6 // ...7 {8 method: "POST",9 matcher: "/admin/promotions/:id",10 additionalDataValidator: {11 custom_name: z.string().nullish(),12 },13 },14 ],15})
The validation schema is the similar to that of the Create Promotion 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:
- If
additional_data.custom_name
is set and it'snull
, theCustom
record linked to the promotion is deleted. - If
additional_data.custom_name
is set and the promotion doesn't have a linkedCustom
record, a new record is created and linked to the promotion. - If
additional_data.custom_name
is set and the promotion has a linkedCustom
record, thecustom_name
property of theCustom
record is updated.
Start by creating the step that updates a Custom
record. Create the file src/workflows/update-custom-from-promotion/steps/update-custom.ts
with the following content:
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-promotion/steps/delete-custom.ts
with the following content:
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-promotion/index.ts
with the following content:
8import { updateCustomStep } from "./steps/update-custom"9 10export type UpdateCustomFromPromotionStepInput = {11 promotion: PromotionDTO12 additional_data?: {13 custom_name?: string | null14 }15}16 17export const updateCustomFromPromotionWorkflow = createWorkflow(18 "update-custom-from-promotion",19 (input: UpdateCustomFromPromotionStepInput) => {20 const { data: promotions } = useQueryGraphStep({21 entity: "promotion",22 fields: ["custom.*"],23 filters: {24 id: input.promotion.id,25 },26 })27 28 // TODO create, update, or delete Custom record29 }30)
The workflow accepts the same input as the promotionsUpdated
workflow hook handler would.
In the workflow, you retrieve the promotion's linked Custom
record using Query.
Next, replace the TODO
with the following:
1const created = when(2 "create-promotion-custom-link",3 {4 input,5 promotions,6 }, (data) => 7 !data.promotions[0].custom && 8 data.input.additional_data?.custom_name?.length > 09)10.then(() => {11 const custom = createCustomStep({12 custom_name: input.additional_data.custom_name,13 })14 15 createRemoteLinkStep([{16 [Modules.PROMOTION]: {17 promotion_id: input.promotion.id,18 },19 [HELLO_MODULE]: {20 custom_id: custom.id,21 },22 }])23 24 return custom25})26 27// TODO update, or delete Custom record
Using when-then
, you check if the promotion 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 promotion.
To create the Custom
record, you use the createCustomStep
you created in an earlier section.
Next, replace the new TODO
with the following:
1const deleted = when(2 "delete-promotion-custom-link",3 {4 input,5 promotions,6 }, (data) => 7 data.promotions[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: promotions[0].custom,15 })16 17 dismissRemoteLinkStep({18 [HELLO_MODULE]: {19 custom_id: promotions[0].custom.id,20 },21 })22 23 return promotions[0].custom.id24})25 26// TODO delete Custom record
Using when-then
, you check if the promotion 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:
1const updated = when({2 input,3 promotions,4}, (data) => data.promotions[0].custom && data.input.additional_data?.custom_name?.length > 0)5.then(() => {6 return updateCustomStep({7 id: promotions[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 promotion 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 promotionsUpdated Workflow Hook#
You can now consume the promotionsUpdated
and execute the workflow you created.
Create the file src/workflows/hooks/promotion-updated.ts
with the following content:
1import { updatePromotionsWorkflow } from "@medusajs/medusa/core-flows"2import { 3 UpdateCustomFromPromotionStepInput, 4 updateCustomFromPromotionWorkflow,5} from "../update-custom-from-promotion"6 7updatePromotionsWorkflow.hooks.promotionsUpdated(8 async ({ promotions, additional_data }, { container }) => {9 const workflow = updateCustomFromPromotionWorkflow(container)10 11 for (const promotion of promotions) {12 await workflow.run({13 input: {14 promotion,15 additional_data,16 } as UpdateCustomFromPromotionStepInput,17 })18 }19 }20)
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/promotions/:id
to update a promotion, passing custom_name
in additional_data
:
Make sure to replace {promotion_id}
with the promotion's ID, and {token}
with the JWT token of an admin user.
The request will return the promotion's details with the updated custom
linked record.