feat: Extending API routes validators (#8254)
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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<typeof AdminCreateDraftOrder>
|
||||
export const AdminCreateDraftOrder = z
|
||||
export type AdminCreateDraftOrderType = z.infer<typeof _AdminCreateDraftOrder>
|
||||
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<any, any>) => {
|
||||
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" }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<typeof AdminCreatePromotion>
|
||||
export const AdminCreatePromotion = z
|
||||
export type AdminCreatePromotionType = z.infer<typeof _AdminCreatePromotion>
|
||||
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<any, any>) => {
|
||||
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<typeof AdminUpdatePromotion>
|
||||
export const AdminUpdatePromotion = z
|
||||
export type AdminUpdatePromotionType = z.infer<typeof _AdminUpdatePromotion>
|
||||
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<any, any>) => {
|
||||
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",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<any, any>) => {
|
||||
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`
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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<any, any>) => {
|
||||
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`
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,7 +4,11 @@ import { MedusaRequest, MedusaResponse } from "../../types/routing"
|
||||
import { zodValidator } from "./zod-helper"
|
||||
|
||||
export function validateAndTransformBody(
|
||||
zodSchema: z.ZodObject<any, any> | z.ZodEffects<any, any>
|
||||
zodSchema:
|
||||
| z.ZodObject<any, any>
|
||||
| ((
|
||||
customSchema?: z.ZodObject<any, any>
|
||||
) => z.ZodObject<any, any> | z.ZodEffects<any, any>)
|
||||
): (
|
||||
req: MedusaRequest,
|
||||
res: MedusaResponse,
|
||||
@@ -12,7 +16,18 @@ export function validateAndTransformBody(
|
||||
) => Promise<void> {
|
||||
return async (req: MedusaRequest, _: MedusaResponse, next: NextFunction) => {
|
||||
try {
|
||||
req.validatedBody = await zodValidator(zodSchema, req.body)
|
||||
let schema: z.ZodObject<any, any> | z.ZodEffects<any, any>
|
||||
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)
|
||||
|
||||
@@ -58,7 +58,11 @@ const getFilterableFields = <T extends RequestQueryFields>(obj: T): T => {
|
||||
}
|
||||
|
||||
export function validateAndTransformQuery<TEntity extends BaseEntity>(
|
||||
zodSchema: z.ZodObject<any, any> | z.ZodEffects<any, any>,
|
||||
zodSchema:
|
||||
| z.ZodObject<any, any>
|
||||
| ((
|
||||
customSchema?: z.ZodObject<any, any>
|
||||
) => z.ZodObject<any, any> | z.ZodEffects<any, any>),
|
||||
queryConfig: QueryConfig<TEntity>
|
||||
): (
|
||||
req: MedusaRequest,
|
||||
@@ -70,7 +74,19 @@ export function validateAndTransformQuery<TEntity extends BaseEntity>(
|
||||
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<any, any> | z.ZodEffects<any, any>
|
||||
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 })
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ZodObject } from "zod"
|
||||
import type { NextFunction, Request, Response } from "express"
|
||||
|
||||
import {
|
||||
@@ -61,6 +62,14 @@ export interface MedusaRequest<Body = unknown>
|
||||
* A generic context object that can be used across the request lifecycle
|
||||
*/
|
||||
context?: Record<string, any>
|
||||
/**
|
||||
* Custom validators for the request body and query params that will be
|
||||
* merged with the original validator of the route.
|
||||
*/
|
||||
extendedValidators?: {
|
||||
body?: ZodObject<any, any>
|
||||
queryParams?: ZodObject<any, any>
|
||||
}
|
||||
}
|
||||
|
||||
export interface AuthContext {
|
||||
|
||||
Reference in New Issue
Block a user