feat: Extending API routes validators (#8254)

This commit is contained in:
Harminder Virk authored and GitHub committed 2024-07-24 14:36:41 +05:30
1 parent 2188a4e1ba
commit ca88f204d4
8 files changed
+198 -17

No files matched your search

@@ -26,7 +26,7 @@ export const AdminGetCampaignsParams = createFindParams({
) )
.strict() .strict()
export const CreateCampaignBudget = z const CreateCampaignBudget = z
.object({ .object({
type: z.nativeEnum(CampaignBudgetType), type: z.nativeEnum(CampaignBudgetType),
limit: z.number().nullish(), 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 { AddressPayload, BigNumberInput } from "../../utils/common-validators"
import { createFindParams, createSelectParams } from "../../utils/validators" import { createFindParams, createSelectParams } from "../../utils/validators"
@@ -49,8 +49,8 @@ const Item = z
return true return true
}) })
export type AdminCreateDraftOrderType = z.infer<typeof AdminCreateDraftOrder> export type AdminCreateDraftOrderType = z.infer<typeof _AdminCreateDraftOrder>
export const AdminCreateDraftOrder = z const _AdminCreateDraftOrder = z
.object({ .object({
status: z.nativeEnum(Status).optional(), status: z.nativeEnum(Status).optional(),
sales_channel_id: z.string().nullish(), sales_channel_id: z.string().nullish(),
@@ -67,7 +67,13 @@ export const AdminCreateDraftOrder = z
metadata: z.record(z.unknown()).nullish(), metadata: z.record(z.unknown()).nullish(),
}) })
.strict() .strict()
.refine(
export const AdminCreateDraftOrder = (customSchema?: ZodObject<any, any>) => {
const schema = customSchema
? _AdminCreateDraftOrder.merge(customSchema)
: _AdminCreateDraftOrder
return schema.refine(
(data) => { (data) => {
if (!data.email && !data.customer_id) { if (!data.email && !data.customer_id) {
return false return false
@@ -77,3 +83,4 @@ export const AdminCreateDraftOrder = z
}, },
{ message: "Either email or customer_id must be provided" } { message: "Either email or customer_id must be provided" }
) )
}
@@ -5,7 +5,7 @@ import {
PromotionRuleOperator, PromotionRuleOperator,
PromotionType, PromotionType,
} from "@medusajs/utils" } from "@medusajs/utils"
import { z } from "zod" import { z, ZodObject } from "zod"
import { import {
createFindParams, createFindParams,
createOperatorMap, createOperatorMap,
@@ -155,8 +155,8 @@ const promoRefinement = (promo) => {
return true return true
} }
export type AdminCreatePromotionType = z.infer<typeof AdminCreatePromotion> export type AdminCreatePromotionType = z.infer<typeof _AdminCreatePromotion>
export const AdminCreatePromotion = z const _AdminCreatePromotion = z
.object({ .object({
code: z.string(), code: z.string(),
is_automatic: z.boolean().optional(), is_automatic: z.boolean().optional(),
@@ -167,14 +167,21 @@ export const AdminCreatePromotion = z
rules: z.array(AdminCreatePromotionRule).optional(), rules: z.array(AdminCreatePromotionRule).optional(),
}) })
.strict() .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 // In the case of a buyget promotion, we require at least one buy rule and quantities
.refine(promoRefinement, { return schema.refine(promoRefinement, {
message: message:
"Buyget promotions require at least one buy rule and quantities to be defined", "Buyget promotions require at least one buy rule and quantities to be defined",
}) })
}
export type AdminUpdatePromotionType = z.infer<typeof AdminUpdatePromotion> export type AdminUpdatePromotionType = z.infer<typeof _AdminUpdatePromotion>
export const AdminUpdatePromotion = z const _AdminUpdatePromotion = z
.object({ .object({
code: z.string().optional(), code: z.string().optional(),
is_automatic: z.boolean().optional(), is_automatic: z.boolean().optional(),
@@ -185,8 +192,15 @@ export const AdminUpdatePromotion = z
rules: z.array(AdminCreatePromotionRule).optional(), rules: z.array(AdminCreatePromotionRule).optional(),
}) })
.strict() .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 // In the case of a buyget promotion, we require at least one buy rule and quantities
.refine(promoRefinement, { return schema.refine(promoRefinement, {
message: message:
"Buyget promotions require at least one buy rule and quantities to be defined", "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 { NextFunction, Request, Response } from "express"
import { createFindParams } from "../validators" import { createFindParams } from "../validators"
import { validateAndTransformQuery } from "../validate-query" import { validateAndTransformQuery } from "../validate-query"
import { MedusaError } from "@medusajs/utils" import { MedusaRequest, MedusaResponse } from "../../../types/routing"
describe("validateAndTransformQuery", () => { describe("validateAndTransformQuery", () => {
afterEach(() => { 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`
)
)
})
}) })
+17 -2
View File
@@ -4,7 +4,11 @@ import { MedusaRequest, MedusaResponse } from "../../types/routing"
import { zodValidator } from "./zod-helper" import { zodValidator } from "./zod-helper"
export function validateAndTransformBody( 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, req: MedusaRequest,
res: MedusaResponse, res: MedusaResponse,
@@ -12,7 +16,18 @@ export function validateAndTransformBody(
) => Promise<void> { ) => Promise<void> {
return async (req: MedusaRequest, _: MedusaResponse, next: NextFunction) => { return async (req: MedusaRequest, _: MedusaResponse, next: NextFunction) => {
try { 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() next()
} catch (e) { } catch (e) {
next(e) next(e)
@@ -58,7 +58,11 @@ const getFilterableFields = <T extends RequestQueryFields>(obj: T): T => {
} }
export function validateAndTransformQuery<TEntity extends BaseEntity>( 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> queryConfig: QueryConfig<TEntity>
): ( ): (
req: MedusaRequest, req: MedusaRequest,
@@ -70,7 +74,19 @@ export function validateAndTransformQuery<TEntity extends BaseEntity>(
const allowed = (req.allowed ?? queryConfig.allowed ?? []) as string[] const allowed = (req.allowed ?? queryConfig.allowed ?? []) as string[]
delete req.allowed delete req.allowed
const query = normalizeQuery(req) 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 const cnf = queryConfig.isList
? prepareListQuery(validated, { ...queryConfig, allowed }) ? prepareListQuery(validated, { ...queryConfig, allowed })
: prepareRetrieveQuery(validated, { ...queryConfig, allowed }) : prepareRetrieveQuery(validated, { ...queryConfig, allowed })
+9
View File
@@ -1,3 +1,4 @@
import { ZodObject } from "zod"
import type { NextFunction, Request, Response } from "express" import type { NextFunction, Request, Response } from "express"
import { import {
@@ -61,6 +62,14 @@ export interface MedusaRequest<Body = unknown>
* A generic context object that can be used across the request lifecycle * A generic context object that can be used across the request lifecycle
*/ */
context?: Record<string, any> 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 { export interface AuthContext {