feat(core-flows): validate hook in cart workflows (#10967)

* feat(core-flows): validate hook

* rm only
This commit is contained in:
Carlos R. L. Rodrigues
2025-01-15 18:49:46 -05:00
committed by GitHub
parent 2a25b4d95f
commit 11f98f374c
14 changed files with 194 additions and 74 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/core-flows": patch
---
feat(core-flows): validation hook on cart mutations
@@ -845,6 +845,11 @@ medusaIntegrationTestRunner({
describe("UpdateCartWorkflow", () => { describe("UpdateCartWorkflow", () => {
it("should remove item with custom price when region is updated", async () => { it("should remove item with custom price when region is updated", async () => {
const hookCallback = jest.fn()
addToCartWorkflow.hooks.validate((data) => {
hookCallback(data)
})
const salesChannel = await scModuleService.createSalesChannels({ const salesChannel = await scModuleService.createSalesChannels({
name: "Webshop", name: "Webshop",
}) })
@@ -926,8 +931,7 @@ medusaIntegrationTestRunner({
select: ["id", "region_id", "currency_code", "sales_channel_id"], select: ["id", "region_id", "currency_code", "sales_channel_id"],
}) })
await addToCartWorkflow(appContainer).run({ const wfInput = {
input: {
items: [ items: [
{ {
variant_id: product.variants[0].id, variant_id: product.variants[0].id,
@@ -948,13 +952,30 @@ medusaIntegrationTestRunner({
}, },
], ],
cart_id: cart.id, cart_id: cart.id,
}, }
await addToCartWorkflow(appContainer).run({
input: wfInput,
}) })
cart = await cartModuleService.retrieveCart(cart.id, { cart = await cartModuleService.retrieveCart(cart.id, {
relations: ["items"], relations: ["items"],
}) })
expect(hookCallback).toHaveBeenCalledWith({
cart: {
completed_at: null,
id: expect.stringContaining("cart_"),
sales_channel_id: expect.stringContaining("sc_"),
currency_code: "usd",
region_id: expect.stringContaining("reg_"),
item_total: 0,
total: 0,
email: null,
customer_id: null,
},
input: wfInput,
})
expect(cart).toEqual( expect(cart).toEqual(
expect.objectContaining({ expect.objectContaining({
id: cart.id, id: cart.id,
@@ -1,5 +1,5 @@
import { CartDTO, CartWorkflowDTO } from "@medusajs/framework/types" import { CartDTO, CartWorkflowDTO } from "@medusajs/framework/types"
import { MedusaError, isPresent } from "@medusajs/framework/utils" import { MedusaError } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk" import { createStep } from "@medusajs/framework/workflows-sdk"
export interface ValidateCartStepInput { export interface ValidateCartStepInput {
@@ -15,13 +15,6 @@ export const validateCartStep = createStep(
async (data: ValidateCartStepInput) => { async (data: ValidateCartStepInput) => {
const { cart } = data const { cart } = data
if (!isPresent(cart)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cart does not exist`
)
}
if (cart.completed_at) { if (cart.completed_at) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.INVALID_DATA, MedusaError.Types.INVALID_DATA,
@@ -1,9 +1,11 @@
import { CartWorkflowEvents, MedusaError } from "@medusajs/framework/utils" import { CartWorkflowEvents, MedusaError } from "@medusajs/framework/utils"
import { import {
createHook,
createWorkflow, createWorkflow,
parallelize, parallelize,
transform, transform,
WorkflowData, WorkflowData,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { emitEventStep } from "../../common/steps/emit-event" import { emitEventStep } from "../../common/steps/emit-event"
import { useRemoteQueryStep } from "../../common/steps/use-remote-query" import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
@@ -33,18 +35,22 @@ export const addShippingMethodToCartWorkflowId = "add-shipping-method-to-cart"
*/ */
export const addShippingMethodToCartWorkflow = createWorkflow( export const addShippingMethodToCartWorkflow = createWorkflow(
addShippingMethodToCartWorkflowId, addShippingMethodToCartWorkflowId,
( (input: WorkflowData<AddShippingMethodToCartWorkflowInput>) => {
input: WorkflowData<AddShippingMethodToCartWorkflowInput>
): WorkflowData<void> => {
const cart = useRemoteQueryStep({ const cart = useRemoteQueryStep({
entry_point: "cart", entry_point: "cart",
fields: cartFieldsForRefreshSteps, fields: cartFieldsForRefreshSteps,
variables: { id: input.cart_id }, variables: { id: input.cart_id },
list: false, list: false,
throw_if_key_not_found: true,
}) })
validateCartStep({ cart }) validateCartStep({ cart })
const validate = createHook("validate", {
input,
cart,
})
const optionIds = transform({ input }, (data) => { const optionIds = transform({ input }, (data) => {
return (data.input.options ?? []).map((i) => i.id) return (data.input.options ?? []).map((i) => i.id)
}) })
@@ -151,5 +157,9 @@ export const addShippingMethodToCartWorkflow = createWorkflow(
refreshCartItemsWorkflow.runAsStep({ refreshCartItemsWorkflow.runAsStep({
input: { cart_id: cart.id }, input: { cart_id: cart.id },
}) })
return new WorkflowResponse(void 0, {
hooks: [validate],
})
} }
) )
@@ -1,11 +1,13 @@
import { AddToCartWorkflowInputDTO } from "@medusajs/framework/types" import { AddToCartWorkflowInputDTO } from "@medusajs/framework/types"
import { CartWorkflowEvents, isDefined } from "@medusajs/framework/utils" import { CartWorkflowEvents, isDefined } from "@medusajs/framework/utils"
import { import {
createHook,
createWorkflow, createWorkflow,
parallelize, parallelize,
transform, transform,
when, when,
WorkflowData, WorkflowData,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "../../common" import { useQueryGraphStep } from "../../common"
import { emitEventStep } from "../../common/steps/emit-event" import { emitEventStep } from "../../common/steps/emit-event"
@@ -50,6 +52,10 @@ export const addToCartWorkflow = createWorkflow(
}) })
validateCartStep({ cart }) validateCartStep({ cart })
const validate = createHook("validate", {
input,
cart,
})
const variantIds = transform({ input }, (data) => { const variantIds = transform({ input }, (data) => {
return (data.input.items ?? []).map((i) => i.variant_id).filter(Boolean) return (data.input.items ?? []).map((i) => i.variant_id).filter(Boolean)
@@ -134,5 +140,9 @@ export const addToCartWorkflow = createWorkflow(
eventName: CartWorkflowEvents.UPDATED, eventName: CartWorkflowEvents.UPDATED,
data: { id: cart.id }, data: { id: cart.id },
}) })
return new WorkflowResponse(void 0, {
hooks: [validate],
})
} }
) )
@@ -8,6 +8,7 @@ import {
OrderWorkflowEvents, OrderWorkflowEvents,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { import {
createHook,
createWorkflow, createWorkflow,
parallelize, parallelize,
transform, transform,
@@ -52,9 +53,7 @@ export const completeCartWorkflow = createWorkflow(
idempotent: true, idempotent: true,
retentionTime: THREE_DAYS, retentionTime: THREE_DAYS,
}, },
( (input: WorkflowData<CompleteCartWorkflowInput>) => {
input: WorkflowData<CompleteCartWorkflowInput>
): WorkflowResponse<{ id: string }> => {
const orderCart = useQueryGraphStep({ const orderCart = useQueryGraphStep({
entity: "order_cart", entity: "order_cart",
fields: ["cart_id", "order_id"], fields: ["cart_id", "order_id"],
@@ -65,10 +64,6 @@ export const completeCartWorkflow = createWorkflow(
return orderCart.data[0]?.order_id return orderCart.data[0]?.order_id
}) })
// If order ID does not exist, we are completing the cart for the first time
const order = when("create-order", { orderId }, ({ orderId }) => {
return !orderId
}).then(() => {
const cart = useRemoteQueryStep({ const cart = useRemoteQueryStep({
entry_point: "cart", entry_point: "cart",
fields: completeCartFields, fields: completeCartFields,
@@ -76,6 +71,15 @@ export const completeCartWorkflow = createWorkflow(
list: false, list: false,
}) })
const validate = createHook("validate", {
input,
cart,
})
// If order ID does not exist, we are completing the cart for the first time
const order = when("create-order", { orderId }, ({ orderId }) => {
return !orderId
}).then(() => {
const paymentSessions = validateCartPaymentsStep({ cart }) const paymentSessions = validateCartPaymentsStep({ cart })
const payment = authorizePaymentSessionStep({ const payment = authorizePaymentSessionStep({
@@ -267,6 +271,8 @@ export const completeCartWorkflow = createWorkflow(
return { id: order?.id ?? orderId } return { id: order?.id ?? orderId }
}) })
return new WorkflowResponse(result) return new WorkflowResponse(result, {
hooks: [validate],
})
} }
) )
@@ -39,7 +39,8 @@ import { updateTaxLinesWorkflow } from "./update-tax-lines"
/** /**
* The data to create the cart, along with custom data that's passed to the workflow's hooks. * The data to create the cart, along with custom data that's passed to the workflow's hooks.
*/ */
export type CreateCartWorkflowInput = CreateCartWorkflowInputDTO & AdditionalData export type CreateCartWorkflowInput = CreateCartWorkflowInputDTO &
AdditionalData
export const createCartWorkflowId = "create-cart" export const createCartWorkflowId = "create-cart"
/** /**
@@ -72,6 +73,7 @@ export const createCartWorkflowId = "create-cart"
* *
* Create a cart specifying region, items, and more. * Create a cart specifying region, items, and more.
* *
* @property hooks.validate - This hook is executed before all operations. You can consume this hook to perform any custom validation.
* @property hooks.cartCreated - This hook is executed after a cart is created. You can consume this hook to perform custom actions on the created cart. * @property hooks.cartCreated - This hook is executed after a cart is created. You can consume this hook to perform custom actions on the created cart.
*/ */
export const createCartWorkflow = createWorkflow( export const createCartWorkflow = createWorkflow(
@@ -206,6 +208,11 @@ export const createCartWorkflow = createWorkflow(
} }
}) })
const validate = createHook("validate", {
input: cartInput,
cart: cartToCreate,
})
const carts = createCartsStep([cartToCreate]) const carts = createCartsStep([cartToCreate])
const cart = transform({ carts }, (data) => data.carts?.[0]) const cart = transform({ carts }, (data) => data.carts?.[0])
@@ -240,7 +247,7 @@ export const createCartWorkflow = createWorkflow(
}) })
return new WorkflowResponse(cart, { return new WorkflowResponse(cart, {
hooks: [cartCreated], hooks: [validate, cartCreated],
}) })
} }
) )
@@ -4,6 +4,7 @@ import {
PromotionActions, PromotionActions,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { import {
createHook,
createWorkflow, createWorkflow,
transform, transform,
when, when,
@@ -71,6 +72,11 @@ export const refreshCartItemsWorkflow = createWorkflow(
validateVariantPricesStep({ variants }) validateVariantPricesStep({ variants })
const validate = createHook("validate", {
input,
cart,
})
const lineItems = transform({ cart, variants }, ({ cart, variants }) => { const lineItems = transform({ cart, variants }, ({ cart, variants }) => {
const items = cart.items.map((item) => { const items = cart.items.map((item) => {
const variant = (variants ?? []).find((v) => v.id === item.variant_id)! const variant = (variants ?? []).find((v) => v.id === item.variant_id)!
@@ -143,6 +149,8 @@ export const refreshCartItemsWorkflow = createWorkflow(
input: { cart_id: cart.id }, input: { cart_id: cart.id },
}) })
return new WorkflowResponse(refetchedCart) return new WorkflowResponse(refetchedCart, {
hooks: [validate],
})
} }
) )
@@ -1,10 +1,12 @@
import { isDefined, isPresent } from "@medusajs/framework/utils" import { isDefined, isPresent } from "@medusajs/framework/utils"
import { import {
createHook,
createWorkflow, createWorkflow,
parallelize, parallelize,
transform, transform,
when, when,
WorkflowData, WorkflowData,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "../../common" import { useQueryGraphStep } from "../../common"
import { removeShippingMethodFromCartStep } from "../steps" import { removeShippingMethodFromCartStep } from "../steps"
@@ -48,6 +50,11 @@ export const refreshCartShippingMethodsWorkflow = createWorkflow(
.filter(Boolean) .filter(Boolean)
) )
const validate = createHook("validate", {
input,
cart,
})
when({ listShippingOptionsInput }, ({ listShippingOptionsInput }) => { when({ listShippingOptionsInput }, ({ listShippingOptionsInput }) => {
return !!listShippingOptionsInput?.length return !!listShippingOptionsInput?.length
}).then(() => { }).then(() => {
@@ -126,5 +133,9 @@ export const refreshCartShippingMethodsWorkflow = createWorkflow(
updateShippingMethodsStep(shippingMethodsData.shippingMethodsToUpdate) updateShippingMethodsStep(shippingMethodsData.shippingMethodsToUpdate)
) )
}) })
return new WorkflowResponse(void 0, {
hooks: [validate],
})
} }
) )
@@ -1,6 +1,8 @@
import { MathBN, isPresent } from "@medusajs/framework/utils" import { MathBN, isPresent } from "@medusajs/framework/utils"
import { import {
WorkflowData, WorkflowData,
WorkflowResponse,
createHook,
createWorkflow, createWorkflow,
parallelize, parallelize,
transform, transform,
@@ -21,9 +23,7 @@ export const refreshPaymentCollectionForCartWorkflowId =
*/ */
export const refreshPaymentCollectionForCartWorkflow = createWorkflow( export const refreshPaymentCollectionForCartWorkflow = createWorkflow(
refreshPaymentCollectionForCartWorkflowId, refreshPaymentCollectionForCartWorkflowId,
( (input: WorkflowData<RefreshPaymentCollectionForCartWorklowInput>) => {
input: WorkflowData<RefreshPaymentCollectionForCartWorklowInput>
): WorkflowData<void> => {
const cart = useRemoteQueryStep({ const cart = useRemoteQueryStep({
entry_point: "cart", entry_point: "cart",
fields: [ fields: [
@@ -43,6 +43,11 @@ export const refreshPaymentCollectionForCartWorkflow = createWorkflow(
list: false, list: false,
}) })
const validate = createHook("validate", {
input,
cart,
})
when({ cart }, ({ cart }) => { when({ cart }, ({ cart }) => {
const valueIsEqual = MathBN.eq( const valueIsEqual = MathBN.eq(
cart.payment_collection?.raw_amount ?? -1, cart.payment_collection?.raw_amount ?? -1,
@@ -89,5 +94,9 @@ export const refreshPaymentCollectionForCartWorkflow = createWorkflow(
updatePaymentCollectionStep(updatePaymentCollectionInput) updatePaymentCollectionStep(updatePaymentCollectionInput)
) )
}) })
return new WorkflowResponse(void 0, {
hooks: [validate],
})
} }
) )
@@ -1,8 +1,10 @@
import { import {
createHook,
createWorkflow, createWorkflow,
transform, transform,
when, when,
WorkflowData, WorkflowData,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "../../common" import { useQueryGraphStep } from "../../common"
import { updateCartsStep } from "../steps" import { updateCartsStep } from "../steps"
@@ -32,6 +34,11 @@ export const transferCartCustomerWorkflow = createWorkflow(
const cart = transform({ cartQuery }, ({ cartQuery }) => cartQuery.data[0]) const cart = transform({ cartQuery }, ({ cartQuery }) => cartQuery.data[0])
const validate = createHook("validate", {
input,
cart,
})
const customerQuery = useQueryGraphStep({ const customerQuery = useQueryGraphStep({
entity: "customer", entity: "customer",
filters: { id: input.customer_id }, filters: { id: input.customer_id },
@@ -72,5 +79,9 @@ export const transferCartCustomerWorkflow = createWorkflow(
}) })
} }
) )
return new WorkflowResponse(void 0, {
hooks: [validate],
})
} }
) )
@@ -1,9 +1,11 @@
import { PromotionActions } from "@medusajs/framework/utils" import { PromotionActions } from "@medusajs/framework/utils"
import { import {
createHook,
createWorkflow, createWorkflow,
parallelize, parallelize,
transform, transform,
WorkflowData, WorkflowData,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { useRemoteQueryStep } from "../../common" import { useRemoteQueryStep } from "../../common"
import { import {
@@ -33,9 +35,7 @@ export const updateCartPromotionsWorkflowId = "update-cart-promotions"
*/ */
export const updateCartPromotionsWorkflow = createWorkflow( export const updateCartPromotionsWorkflow = createWorkflow(
updateCartPromotionsWorkflowId, updateCartPromotionsWorkflowId,
( (input: WorkflowData<UpdateCartPromotionsWorkflowInput>) => {
input: WorkflowData<UpdateCartPromotionsWorkflowInput>
): WorkflowData<void> => {
const cart = useRemoteQueryStep({ const cart = useRemoteQueryStep({
entry_point: "cart", entry_point: "cart",
fields: cartFieldsForRefreshSteps, fields: cartFieldsForRefreshSteps,
@@ -43,6 +43,11 @@ export const updateCartPromotionsWorkflow = createWorkflow(
list: false, list: false,
}) })
const validate = createHook("validate", {
input,
cart,
})
const promo_codes = transform({ input }, (data) => { const promo_codes = transform({ input }, (data) => {
return (data.input.promo_codes || []) as string[] return (data.input.promo_codes || []) as string[]
}) })
@@ -85,5 +90,9 @@ export const updateCartPromotionsWorkflow = createWorkflow(
action: PromotionActions.REPLACE, action: PromotionActions.REPLACE,
}) })
) )
return new WorkflowResponse(void 0, {
hooks: [validate],
})
} }
) )
@@ -32,7 +32,8 @@ import { refreshCartItemsWorkflow } from "./refresh-cart-items"
/** /**
* The data to update the cart, along with custom data that's passed to the workflow's hooks. * The data to update the cart, along with custom data that's passed to the workflow's hooks.
*/ */
export type UpdateCartWorkflowInput = UpdateCartWorkflowInputDTO & AdditionalData export type UpdateCartWorkflowInput = UpdateCartWorkflowInputDTO &
AdditionalData
export const updateCartWorkflowId = "update-cart" export const updateCartWorkflowId = "update-cart"
/** /**
@@ -75,6 +76,7 @@ export const updateCartWorkflowId = "update-cart"
* *
* Update a cart's details, such as region, address, and more. * Update a cart's details, such as region, address, and more.
* *
* @property hooks.validate - This hook is executed before all operations. You can consume this hook to perform any custom validation.
* @property hooks.cartUpdated - This hook is executed after a cart is update. You can consume this hook to perform custom actions on the updated cart. * @property hooks.cartUpdated - This hook is executed after a cart is update. You can consume this hook to perform custom actions on the updated cart.
*/ */
export const updateCartWorkflow = createWorkflow( export const updateCartWorkflow = createWorkflow(
@@ -205,6 +207,11 @@ export const updateCartWorkflow = createWorkflow(
} }
) )
const validate = createHook("validate", {
input: cartInput,
cart: cartToUpdate,
})
/* /*
when({ cartInput }, ({ cartInput }) => { when({ cartInput }, ({ cartInput }) => {
return isDefined(cartInput.customer_id) || isDefined(cartInput.email) return isDefined(cartInput.customer_id) || isDefined(cartInput.email)
@@ -274,7 +281,7 @@ export const updateCartWorkflow = createWorkflow(
}) })
return new WorkflowResponse(void 0, { return new WorkflowResponse(void 0, {
hooks: [cartUpdated], hooks: [validate, cartUpdated],
}) })
} }
) )
@@ -1,10 +1,12 @@
import { UpdateLineItemInCartWorkflowInputDTO } from "@medusajs/framework/types" import { UpdateLineItemInCartWorkflowInputDTO } from "@medusajs/framework/types"
import { isDefined, MedusaError } from "@medusajs/framework/utils" import { isDefined, MedusaError } from "@medusajs/framework/utils"
import { import {
createHook,
createWorkflow, createWorkflow,
transform, transform,
when, when,
WorkflowData, WorkflowData,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "../../common" import { useQueryGraphStep } from "../../common"
import { useRemoteQueryStep } from "../../common/steps/use-remote-query" import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
@@ -41,6 +43,11 @@ export const updateLineItemInCartWorkflow = createWorkflow(
validateCartStep({ cart }) validateCartStep({ cart })
const validate = createHook("validate", {
input,
cart,
})
const variantIds = transform({ item }, ({ item }) => { const variantIds = transform({ item }, ({ item }) => {
return [item.variant_id].filter(Boolean) return [item.variant_id].filter(Boolean)
}) })
@@ -63,7 +70,9 @@ export const updateLineItemInCartWorkflow = createWorkflow(
validateVariantPricesStep({ variants }) validateVariantPricesStep({ variants })
const items = transform({ input, item }, (data) => { const items = transform({ input, item }, (data) => {
return [Object.assign(data.item, { quantity: data.input.update.quantity })] return [
Object.assign(data.item, { quantity: data.input.update.quantity }),
]
}) })
confirmVariantInventoryWorkflow.runAsStep({ confirmVariantInventoryWorkflow.runAsStep({
@@ -115,5 +124,9 @@ export const updateLineItemInCartWorkflow = createWorkflow(
refreshCartItemsWorkflow.runAsStep({ refreshCartItemsWorkflow.runAsStep({
input: { cart_id: input.cart_id }, input: { cart_id: input.cart_id },
}) })
return new WorkflowResponse(void 0, {
hooks: [validate],
})
} }
) )