diff --git a/packages/medusa/src/api/admin/campaigns/validators.ts b/packages/medusa/src/api/admin/campaigns/validators.ts index 2c37971505..ddd0b03612 100644 --- a/packages/medusa/src/api/admin/campaigns/validators.ts +++ b/packages/medusa/src/api/admin/campaigns/validators.ts @@ -26,7 +26,7 @@ export const AdminGetCampaignsParams = createFindParams({ ) .strict() -export const CreateCampaignBudget = z +const CreateCampaignBudget = z .object({ type: z.nativeEnum(CampaignBudgetType), limit: z.number().nullish(), diff --git a/packages/medusa/src/api/admin/draft-orders/validators.ts b/packages/medusa/src/api/admin/draft-orders/validators.ts index 70196bb7c4..a008044e1a 100644 --- a/packages/medusa/src/api/admin/draft-orders/validators.ts +++ b/packages/medusa/src/api/admin/draft-orders/validators.ts @@ -1,4 +1,4 @@ -import { z } from "zod" +import { z, ZodObject } from "zod" import { AddressPayload, BigNumberInput } from "../../utils/common-validators" import { createFindParams, createSelectParams } from "../../utils/validators" @@ -49,8 +49,8 @@ const Item = z return true }) -export type AdminCreateDraftOrderType = z.infer -export const AdminCreateDraftOrder = z +export type AdminCreateDraftOrderType = z.infer +const _AdminCreateDraftOrder = z .object({ status: z.nativeEnum(Status).optional(), sales_channel_id: z.string().nullish(), @@ -67,7 +67,13 @@ export const AdminCreateDraftOrder = z metadata: z.record(z.unknown()).nullish(), }) .strict() - .refine( + +export const AdminCreateDraftOrder = (customSchema?: ZodObject) => { + const schema = customSchema + ? _AdminCreateDraftOrder.merge(customSchema) + : _AdminCreateDraftOrder + + return schema.refine( (data) => { if (!data.email && !data.customer_id) { return false @@ -77,3 +83,4 @@ export const AdminCreateDraftOrder = z }, { message: "Either email or customer_id must be provided" } ) +} diff --git a/packages/medusa/src/api/admin/promotions/validators.ts b/packages/medusa/src/api/admin/promotions/validators.ts index e3d779b172..8060dbd3da 100644 --- a/packages/medusa/src/api/admin/promotions/validators.ts +++ b/packages/medusa/src/api/admin/promotions/validators.ts @@ -5,7 +5,7 @@ import { PromotionRuleOperator, PromotionType, } from "@medusajs/utils" -import { z } from "zod" +import { z, ZodObject } from "zod" import { createFindParams, createOperatorMap, @@ -155,8 +155,8 @@ const promoRefinement = (promo) => { return true } -export type AdminCreatePromotionType = z.infer -export const AdminCreatePromotion = z +export type AdminCreatePromotionType = z.infer +const _AdminCreatePromotion = z .object({ code: z.string(), is_automatic: z.boolean().optional(), @@ -167,14 +167,21 @@ export const AdminCreatePromotion = z rules: z.array(AdminCreatePromotionRule).optional(), }) .strict() + +export const AdminCreatePromotion = (customSchema?: ZodObject) => { + const schema = customSchema + ? _AdminCreatePromotion.merge(customSchema) + : _AdminCreatePromotion + // In the case of a buyget promotion, we require at least one buy rule and quantities - .refine(promoRefinement, { + return schema.refine(promoRefinement, { message: "Buyget promotions require at least one buy rule and quantities to be defined", }) +} -export type AdminUpdatePromotionType = z.infer -export const AdminUpdatePromotion = z +export type AdminUpdatePromotionType = z.infer +const _AdminUpdatePromotion = z .object({ code: z.string().optional(), is_automatic: z.boolean().optional(), @@ -185,8 +192,15 @@ export const AdminUpdatePromotion = z rules: z.array(AdminCreatePromotionRule).optional(), }) .strict() + +export const AdminUpdatePromotion = (customSchema?: ZodObject) => { + const schema = customSchema + ? _AdminUpdatePromotion.merge(customSchema) + : _AdminUpdatePromotion + // In the case of a buyget promotion, we require at least one buy rule and quantities - .refine(promoRefinement, { + return schema.refine(promoRefinement, { message: "Buyget promotions require at least one buy rule and quantities to be defined", }) +} diff --git a/packages/medusa/src/api/utils/__tests__/validate-body.spec.ts b/packages/medusa/src/api/utils/__tests__/validate-body.spec.ts new file mode 100644 index 0000000000..0706172a76 --- /dev/null +++ b/packages/medusa/src/api/utils/__tests__/validate-body.spec.ts @@ -0,0 +1,65 @@ +import zod from "zod" +import { MedusaError } from "@medusajs/utils" +import { createLinkBody } from "../validators" +import { validateAndTransformBody } from "../validate-body" +import { MedusaRequest, MedusaResponse } from "../../../types/routing" + +describe("validateAndTransformBody", () => { + afterEach(() => { + jest.clearAllMocks() + }) + + it("should merge a custom validators schema", async () => { + let mockRequest = { + query: {}, + body: {}, + } as MedusaRequest + + const mockResponse = {} as MedusaResponse + const nextFunction = jest.fn() + + mockRequest.extendedValidators = { + body: zod.object({ + brand_id: zod.number(), + }), + } + + let middleware = validateAndTransformBody(createLinkBody()) + await middleware(mockRequest, mockResponse, nextFunction) + expect(nextFunction).toHaveBeenCalledWith( + new MedusaError( + "invalid_data", + `Invalid request: Field 'brand_id' is required` + ) + ) + }) + + it("should pass schema to merge to the original validator factory", async () => { + let mockRequest = { + query: {}, + body: {}, + } as MedusaRequest + + const mockResponse = {} as MedusaResponse + const nextFunction = jest.fn() + + mockRequest.extendedValidators = { + body: zod.object({ + brand_id: zod.number(), + }), + } + + const validatorFactory = (schema?: Zod.ZodObject) => { + return schema ? createLinkBody().merge(schema) : createLinkBody() + } + + let middleware = validateAndTransformBody(validatorFactory) + await middleware(mockRequest, mockResponse, nextFunction) + expect(nextFunction).toHaveBeenCalledWith( + new MedusaError( + "invalid_data", + `Invalid request: Field 'brand_id' is required` + ) + ) + }) +}) diff --git a/packages/medusa/src/api/utils/__tests__/validate-query.spec.ts b/packages/medusa/src/api/utils/__tests__/validate-query.spec.ts index 815e087aa8..b804473c6c 100644 --- a/packages/medusa/src/api/utils/__tests__/validate-query.spec.ts +++ b/packages/medusa/src/api/utils/__tests__/validate-query.spec.ts @@ -1,7 +1,10 @@ +import zod from "zod" +import { MedusaError } from "@medusajs/utils" import { NextFunction, Request, Response } from "express" + import { createFindParams } from "../validators" import { validateAndTransformQuery } from "../validate-query" -import { MedusaError } from "@medusajs/utils" +import { MedusaRequest, MedusaResponse } from "../../../types/routing" describe("validateAndTransformQuery", () => { afterEach(() => { @@ -692,4 +695,56 @@ describe("validateAndTransformQuery", () => { ) ) }) + + it("should merge a custom validators schema", async () => { + let mockRequest = { + query: {}, + } as MedusaRequest + + const mockResponse = {} as MedusaResponse + const nextFunction = jest.fn() + + mockRequest.extendedValidators = { + queryParams: zod.object({ + page: zod.number(), + }), + } + + let middleware = validateAndTransformQuery(createFindParams(), {}) + await middleware(mockRequest, mockResponse, nextFunction) + expect(nextFunction).toHaveBeenCalledWith( + new MedusaError( + "invalid_data", + `Invalid request: Field 'page' is required` + ) + ) + }) + + it("should pass schema to merge to the original validator factory", async () => { + let mockRequest = { + query: {}, + } as MedusaRequest + + const mockResponse = {} as MedusaResponse + const nextFunction = jest.fn() + + mockRequest.extendedValidators = { + queryParams: zod.object({ + page: zod.number(), + }), + } + + const validatorFactory = (schema?: Zod.ZodObject) => { + return schema ? createFindParams().merge(schema) : createFindParams() + } + + let middleware = validateAndTransformQuery(validatorFactory, {}) + await middleware(mockRequest, mockResponse, nextFunction) + expect(nextFunction).toHaveBeenCalledWith( + new MedusaError( + "invalid_data", + `Invalid request: Field 'page' is required` + ) + ) + }) }) diff --git a/packages/medusa/src/api/utils/validate-body.ts b/packages/medusa/src/api/utils/validate-body.ts index 30c9dd615c..af4186f386 100644 --- a/packages/medusa/src/api/utils/validate-body.ts +++ b/packages/medusa/src/api/utils/validate-body.ts @@ -4,7 +4,11 @@ import { MedusaRequest, MedusaResponse } from "../../types/routing" import { zodValidator } from "./zod-helper" export function validateAndTransformBody( - zodSchema: z.ZodObject | z.ZodEffects + zodSchema: + | z.ZodObject + | (( + customSchema?: z.ZodObject + ) => z.ZodObject | z.ZodEffects) ): ( req: MedusaRequest, res: MedusaResponse, @@ -12,7 +16,18 @@ export function validateAndTransformBody( ) => Promise { return async (req: MedusaRequest, _: MedusaResponse, next: NextFunction) => { try { - req.validatedBody = await zodValidator(zodSchema, req.body) + let schema: z.ZodObject | z.ZodEffects + const { body: bodyValidatorToMerge } = req.extendedValidators ?? {} + + if (typeof zodSchema === "function") { + schema = zodSchema(bodyValidatorToMerge) + } else if (bodyValidatorToMerge) { + schema = zodSchema.merge(bodyValidatorToMerge) + } else { + schema = zodSchema + } + + req.validatedBody = await zodValidator(schema, req.body) next() } catch (e) { next(e) diff --git a/packages/medusa/src/api/utils/validate-query.ts b/packages/medusa/src/api/utils/validate-query.ts index e9fedaa0e5..e34b335023 100644 --- a/packages/medusa/src/api/utils/validate-query.ts +++ b/packages/medusa/src/api/utils/validate-query.ts @@ -58,7 +58,11 @@ const getFilterableFields = (obj: T): T => { } export function validateAndTransformQuery( - zodSchema: z.ZodObject | z.ZodEffects, + zodSchema: + | z.ZodObject + | (( + customSchema?: z.ZodObject + ) => z.ZodObject | z.ZodEffects), queryConfig: QueryConfig ): ( req: MedusaRequest, @@ -70,7 +74,19 @@ export function validateAndTransformQuery( const allowed = (req.allowed ?? queryConfig.allowed ?? []) as string[] delete req.allowed const query = normalizeQuery(req) - const validated = await zodValidator(zodSchema, query) + + let schema: z.ZodObject | z.ZodEffects + const { queryParams: queryParamsToMerge } = req.extendedValidators ?? {} + + if (typeof zodSchema === "function") { + schema = zodSchema(queryParamsToMerge) + } else if (queryParamsToMerge) { + schema = zodSchema.merge(queryParamsToMerge) + } else { + schema = zodSchema + } + + const validated = await zodValidator(schema, query) const cnf = queryConfig.isList ? prepareListQuery(validated, { ...queryConfig, allowed }) : prepareRetrieveQuery(validated, { ...queryConfig, allowed }) diff --git a/packages/medusa/src/types/routing.ts b/packages/medusa/src/types/routing.ts index 55b3c21437..0f604ea156 100644 --- a/packages/medusa/src/types/routing.ts +++ b/packages/medusa/src/types/routing.ts @@ -1,3 +1,4 @@ +import { ZodObject } from "zod" import type { NextFunction, Request, Response } from "express" import { @@ -61,6 +62,14 @@ export interface MedusaRequest * A generic context object that can be used across the request lifecycle */ context?: Record + /** + * Custom validators for the request body and query params that will be + * merged with the original validator of the route. + */ + extendedValidators?: { + body?: ZodObject + queryParams?: ZodObject + } } export interface AuthContext {