2.2.2. Guide: Extend Create Product Flow

After linking the custom Brand data model and Medusa's Product Module in the previous chapter, you'll extend the create product workflow and API route to allow associating a brand with a product.

Some API routes, including the Create Product API route, accept an additional_data request body parameter. This parameter can hold custom data that's passed to the hooks of the workflow executed in the API route, allowing you to consume those hooks and perform actions with the custom data.

So, in this chapter, to extend the create product flow and associate a brand with a product, you will:

  • Consume the productsCreated hook of the createProductsWorkflow, which is executed within the workflow after the product is created. You'll link the product with the brand passed in the additional_data parameter.
  • Extend the Create Product API route to allow passing a brand ID in additional_data.
NoteTo learn more about the additional_data property and the API routes that accept additional data, refer to this chapter.

1. Consume the productCreated Hook#

A workflow hook is a point in a workflow where you can inject a step to perform a custom functionality. Consuming a workflow hook allows you to extend the features of a workflow and, consequently, the API route that uses it.

NoteLearn more about the workflow hooks in this chapter.

The createProductsWorkflow used in the Create Product API route has a productsCreated hook that runs after the product is created. You'll consume this hook to link the created product with the brand specified in the request parameters.

To consume the productsCreated hook, create the file src/workflows/hooks/created-product.ts with the following content:

Directory structure after creating the hook's file.

src/workflows/hooks/created-product.ts
1import { createProductsWorkflow } from "@medusajs/medusa/core-flows"2import { StepResponse } from "@medusajs/framework/workflows-sdk"3import { Modules } from "@medusajs/framework/utils"4import { LinkDefinition } from "@medusajs/framework/types"5import { BRAND_MODULE } from "../../modules/brand"6import BrandModuleService from "../../modules/brand/service"7
8createProductsWorkflow.hooks.productsCreated(9  (async ({ products, additional_data }, { container }) => {10    if (!additional_data?.brand_id) {11      return new StepResponse([], [])12    }13
14    const brandModuleService: BrandModuleService = container.resolve(15      BRAND_MODULE16    )17    // if the brand doesn't exist, an error is thrown.18    await brandModuleService.retrieveBrand(additional_data.brand_id as string)19
20    // TODO link brand to product21  })22)

Workflows have a special hooks property to access its hooks and consume them. Each hook, such as productCreated, accepts a step function as a parameter. The step function accepts the following parameters:

  1. An object having an additional_data property, which is the custom data passed in the request body under additional_data. The object will also have properties passed from the workflow to the hook, which in this case is the products property that holds an array of the created products.
  2. An object of properties related to the step's context. It has a container property whose value is the Medusa container to resolve framework and commerce tools.

In the step, if a brand ID is passed in additional_data, you resolve the Brand Module's service and use its generated retrieveBrand method to retrieve the brand by its ID. The retrieveBrand method will throw an error if the brand doesn't exist.

Next, you want to create a link between the created products and the brand. To do so, you use Remote Link, which is a class from the Modules SDK that provides methods to manage linked records.

NoteLearn more about the remote link in this chapter.

To use the remote link in the productCreated hook, replace the TODO with the following:

src/workflows/hooks/created-product.ts
1const remoteLink = container.resolve("remoteLink")2const logger = container.resolve("logger")3
4const links: LinkDefinition[] = []5
6for (const product of products) {7  links.push({8    [Modules.PRODUCT]: {9      product_id: product.id,10    },11    [BRAND_MODULE]: {12      brand_id: additional_data.brand_id,13    },14  })15}16
17await remoteLink.create(links)18
19logger.info("Linked brand to products")20
21return new StepResponse(links, links)

You resolve the remote link from the container. Then you loop over the created products to assemble an array of links to be created. After that, you pass the array of links to remote link's create method, which will link the product and brand records.

Each property in the link object is the name of a module, and its value is an object having a {model_name}_id property, where {model_name} is the snake-case name of the module's data model. Its value is the ID of the record to be linked. The link object's properties must be set in the same order as the link configurations passed to defineLink.

Diagram showcasing how the order of defining a link affects creating the link

Finally, you return an instance of StepResponse returning the created links.

You can pass as a second parameter of the hook a compensation function that undoes what the step did. It receives as a first parameter the returned StepResponse's second parameter, and the step context object as a second parameter.

To undo creating the links in the hook, pass the following compensation function as a second parameter to productsCreated:

src/workflows/hooks/created-product.ts
1createProductsWorkflow.hooks.productsCreated(2  // ...3  (async (links, { container }) => {4    if (!links?.length) {5      return6    }7
8    const remoteLink = container.resolve("remoteLink")9
10    await remoteLink.dismiss(links)11  })12)

In the compensation function, if the links parameter isn't empty, you resolve remote link from the container and use its dismiss method. This method removes a link between two records. It accepts the same parameter as the create method.


2. Configure Additional Data Validation#

Now that you've consumed the productCreated hook, you want to configure the /admin/products API route that creates a new product to accept a brand ID in its additional_data parameter.

You configure the properties accepted in additional_data in the src/api/middlewares.ts that exports middleware configurations. So, create the file (or, if already existing, add to the file) src/api/middlewares.ts the following content:

Directory structure after adding the middelwares file

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

Objects in routes accept an additionalDataValidator property that configures the validation rules for custom properties passed in the additional_data request parameter. It accepts an object whose keys are custom property names, and their values are validation rules created using Zod.

So, POST requests sent to /admin/products can now pass the ID of a brand in the brand_id property of additional_data.


Test it Out#

To test it out, first, retrieve the authentication token of your admin user by sending a POST request to /auth/user/emailpass:

Code
1curl -X POST 'http://localhost:9000/auth/user/emailpass' \2-H 'Content-Type: application/json' \3--data-raw '{4    "email": "admin@medusa-test.com",5    "password": "supersecret"6}'

Make sure to replace the email and password in the request body with your user's credentials.

Then, send a POST request to /admin/products to create a product, and pass in the additional_data parameter a brand's ID:

Code
1curl -X POST 'http://localhost:9000/admin/products' \2-H 'Content-Type: application/json' \3-H 'Authorization: Bearer {token}' \4--data '{5    "title": "Product 1",6    "options": [7      {8        "title": "Default option",9        "values": ["Default option value"]10      }11    ],12    "additional_data": {13        "brand_id": "{brand_id}"14    }15}'

Make sure to replace {token} with the token you received from the previous request, and {brand_id} with the ID of a brand in your application.

The request creates a product and returns it.

In the Medusa application's logs, you'll find the message Linked brand to products, indicating that the workflow hook handler ran and linked the brand to the products.


Next Steps: Query Linked Brands and Products#

Now that you've extending the create-product flow to link a brand to it, you want to retrieve the brand details of a product. You'll learn how to do so in the next chapter.

Was this chapter helpful?
Edit this page