feature: introduce additional_data to the product endpoints (#8405)

This commit is contained in:
Harminder Virk
2024-08-05 09:24:49 +05:30
committed by GitHub
parent bcad5052af
commit 0706bab663
30 changed files with 219 additions and 258 deletions

View File

@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types"
import { AdditionalData, ProductTypes } from "@medusajs/types"
import {
WorkflowData,
WorkflowResponse,
@@ -9,8 +9,7 @@ import { createCollectionsStep } from "../steps"
type WorkflowInput = {
collections: ProductTypes.CreateProductCollectionDTO[]
additional_data?: Record<string, unknown>
}
} & AdditionalData
export const createCollectionsWorkflowId = "create-collections"
export const createCollectionsWorkflow = createWorkflow(

View File

@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types"
import { AdditionalData, ProductTypes } from "@medusajs/types"
import {
WorkflowData,
WorkflowResponse,
@@ -9,8 +9,7 @@ import { createProductOptionsStep } from "../steps"
type WorkflowInput = {
product_options: ProductTypes.CreateProductOptionDTO[]
additional_data?: Record<string, unknown>
}
} & AdditionalData
export const createProductOptionsWorkflowId = "create-product-options"

View File

@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types"
import { AdditionalData, ProductTypes } from "@medusajs/types"
import {
WorkflowData,
WorkflowResponse,
@@ -9,8 +9,7 @@ import { createProductTagsStep } from "../steps"
type WorkflowInput = {
product_tags: ProductTypes.CreateProductTagDTO[]
additional_data?: Record<string, unknown>
}
} & AdditionalData
export const createProductTagsWorkflowId = "create-product-tags"

View File

@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types"
import { AdditionalData, ProductTypes } from "@medusajs/types"
import {
WorkflowData,
WorkflowResponse,
@@ -9,8 +9,7 @@ import { createProductTypesStep } from "../steps"
type WorkflowInput = {
product_types: ProductTypes.CreateProductTypeDTO[]
additional_data?: Record<string, unknown>
}
} & AdditionalData
export const createProductTypesWorkflowId = "create-product-types"

View File

@@ -1,5 +1,10 @@
import { LinkDefinition } from "@medusajs/modules-sdk"
import { InventoryTypes, PricingTypes, ProductTypes } from "@medusajs/types"
import {
AdditionalData,
InventoryTypes,
PricingTypes,
ProductTypes,
} from "@medusajs/types"
import {
WorkflowData,
WorkflowResponse,
@@ -25,8 +30,7 @@ type WorkflowInput = {
required_quantity?: number
}[]
})[]
additional_data?: Record<string, unknown>
}
} & AdditionalData
const buildLink = (
variant_id: string,

View File

@@ -1,4 +1,5 @@
import {
AdditionalData,
CreateProductWorkflowInputDTO,
PricingTypes,
ProductTypes,
@@ -17,8 +18,7 @@ import { createProductVariantsWorkflow } from "./create-product-variants"
type WorkflowInput = {
products: CreateProductWorkflowInputDTO[]
additional_data?: Record<string, unknown>
}
} & AdditionalData
export const createProductsWorkflowId = "create-products"

View File

@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types"
import { AdditionalData, ProductTypes } from "@medusajs/types"
import {
WorkflowData,
WorkflowResponse,
@@ -10,8 +10,7 @@ import { updateCollectionsStep } from "../steps"
type UpdateCollectionsStepInput = {
selector: ProductTypes.FilterableProductCollectionProps
update: ProductTypes.UpdateProductCollectionDTO
additional_data?: Record<string, unknown>
}
} & AdditionalData
type WorkflowInput = UpdateCollectionsStepInput

View File

@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types"
import { AdditionalData, ProductTypes } from "@medusajs/types"
import {
WorkflowData,
WorkflowResponse,
@@ -10,8 +10,7 @@ import { updateProductOptionsStep } from "../steps"
type UpdateProductOptionsStepInput = {
selector: ProductTypes.FilterableProductOptionProps
update: ProductTypes.UpdateProductOptionDTO
additional_data?: Record<string, unknown>
}
} & AdditionalData
type WorkflowInput = UpdateProductOptionsStepInput

View File

@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types"
import { AdditionalData, ProductTypes } from "@medusajs/types"
import {
WorkflowData,
WorkflowResponse,
@@ -10,8 +10,7 @@ import { updateProductTagsStep } from "../steps"
type UpdateProductTagsStepInput = {
selector: ProductTypes.FilterableProductTypeProps
update: ProductTypes.UpdateProductTypeDTO
additional_data?: Record<string, unknown>
}
} & AdditionalData
type WorkflowInput = UpdateProductTagsStepInput

View File

@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types"
import { AdditionalData, ProductTypes } from "@medusajs/types"
import {
WorkflowData,
WorkflowResponse,
@@ -10,8 +10,7 @@ import { updateProductTypesStep } from "../steps"
type UpdateProductTypesStepInput = {
selector: ProductTypes.FilterableProductTypeProps
update: ProductTypes.UpdateProductTypeDTO
additional_data?: Record<string, unknown>
}
} & AdditionalData
type WorkflowInput = UpdateProductTypesStepInput

View File

@@ -1,4 +1,4 @@
import { PricingTypes, ProductTypes } from "@medusajs/types"
import { AdditionalData, PricingTypes, ProductTypes } from "@medusajs/types"
import {
WorkflowData,
WorkflowResponse,
@@ -23,9 +23,7 @@ type UpdateProductVariantsStepInput =
})[]
}
type WorkflowInput = UpdateProductVariantsStepInput & {
additional_data?: Record<string, unknown>
}
type WorkflowInput = UpdateProductVariantsStepInput & AdditionalData
export const updateProductVariantsWorkflowId = "update-product-variants"
export const updateProductVariantsWorkflow = createWorkflow(

View File

@@ -1,6 +1,7 @@
import { updateProductsStep } from "../steps/update-products"
import {
AdditionalData,
CreateMoneyAmountDTO,
ProductTypes,
UpdateProductVariantWorkflowInputDTO,
@@ -26,16 +27,14 @@ type UpdateProductsStepInputSelector = {
sales_channels?: { id: string }[]
variants?: UpdateProductVariantWorkflowInputDTO[]
}
additional_data?: Record<string, unknown>
}
} & AdditionalData
type UpdateProductsStepInputProducts = {
products: (Omit<ProductTypes.UpsertProductDTO, "variants"> & {
sales_channels?: { id: string }[]
variants?: UpdateProductVariantWorkflowInputDTO[]
})[]
additional_data?: Record<string, unknown>
}
} & AdditionalData
type UpdateProductsStepInput =
| UpdateProductsStepInputSelector

View File

@@ -0,0 +1,7 @@
/**
* Represents the additional_data property accepted in HTTP
* requests to allow arbitrary values
*/
export type AdditionalData = {
additional_data?: Record<string, unknown>
}

View File

@@ -1,2 +1,3 @@
export * from "./request"
export * from "./response"
export * from "./additional_data"

View File

@@ -147,14 +147,12 @@ 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.
* Custom validator to validate the `additional_data` property in
* requests that allows for additional_data
*/
extendedValidators?: {
body?: ZodObject<any, any>
queryParams?: ZodObject<any, any>
}
additionalDataValidator?: ZodObject<any, any>
}
export interface AuthContext {

View File

@@ -7,7 +7,7 @@ import {
MiddlewareVerb,
ParserConfig,
} from "../types"
import { ZodObject } from "zod"
import zod, { ZodRawShape } from "zod"
/**
* A helper function to configure the routes by defining custom middleware,
@@ -19,10 +19,7 @@ export function defineMiddlewares<
method?: MiddlewareVerb | MiddlewareVerb[]
matcher: string | RegExp
bodyParser?: ParserConfig
extendedValidators?: {
body?: ZodObject<any, any>
queryParams?: ZodObject<any, any>
}
additionalDataValidator?: ZodRawShape
// eslint-disable-next-line space-before-function-paren
middlewares?: (<Req extends MedusaRequest>(
req: Req,
@@ -41,17 +38,16 @@ export function defineMiddlewares<
return {
errorHandler,
routes: routes.map((route) => {
const { middlewares, extendedValidators, ...rest } = route
const { middlewares, additionalDataValidator, ...rest } = route
const customMiddleware: MedusaRequestHandler[] = []
/**
* Define a custom validator when "extendedValidators.body" or
* "extendedValidators.queryParams" validation schema is
* provided.
* Define a custom validator when a zod schema is provided via
* "additionalDataValidator" property
*/
if (extendedValidators?.body || extendedValidators?.queryParams) {
if (additionalDataValidator) {
customMiddleware.push((req, _, next) => {
req.extendedValidators = extendedValidators
req.additionalDataValidator = zod.object(additionalDataValidator)
next()
})
}

View File

@@ -8,7 +8,7 @@ import {
} from "@medusajs/core-flows"
import { remapKeysForProduct, remapProductResponse } from "../../../helpers"
import { HttpTypes } from "@medusajs/types"
import { AdditionalData, HttpTypes } from "@medusajs/types"
import { refetchEntity } from "../../../../../utils/refetch-entity"
export const GET = async (
@@ -28,16 +28,20 @@ export const GET = async (
}
export const POST = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminUpdateProductOption>,
req: AuthenticatedMedusaRequest<
HttpTypes.AdminUpdateProductOption & AdditionalData
>,
res: MedusaResponse<HttpTypes.AdminProductResponse>
) => {
const productId = req.params.id
const optionId = req.params.option_id
const { additional_data, ...update } = req.validatedBody
await updateProductOptionsWorkflow(req.scope).run({
input: {
selector: { id: optionId, product_id: productId },
update: req.validatedBody,
update,
additional_data,
},
})

View File

@@ -5,7 +5,7 @@ import {
import { createProductOptionsWorkflow } from "@medusajs/core-flows"
import { remapKeysForProduct, remapProductResponse } from "../../helpers"
import { HttpTypes } from "@medusajs/types"
import { AdditionalData, HttpTypes } from "@medusajs/types"
import {
refetchEntities,
refetchEntity,
@@ -33,18 +33,23 @@ export const GET = async (
}
export const POST = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminCreateProductOption>,
req: AuthenticatedMedusaRequest<
HttpTypes.AdminCreateProductOption & AdditionalData
>,
res: MedusaResponse<HttpTypes.AdminProductResponse>
) => {
const productId = req.params.id
const { additional_data, ...rest } = req.validatedBody
await createProductOptionsWorkflow(req.scope).run({
input: {
product_options: [
{
...req.validatedBody,
...rest,
product_id: productId,
},
],
additional_data,
},
})

View File

@@ -8,7 +8,7 @@ import {
} from "../../../../types/routing"
import { remapKeysForProduct, remapProductResponse } from "../helpers"
import { MedusaError } from "@medusajs/utils"
import { HttpTypes } from "@medusajs/types"
import { AdditionalData, HttpTypes } from "@medusajs/types"
import { refetchEntity } from "../../../utils/refetch-entity"
export const GET = async (
@@ -31,13 +31,18 @@ export const GET = async (
}
export const POST = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminUpdateProduct>,
req: AuthenticatedMedusaRequest<
HttpTypes.AdminUpdateProduct & AdditionalData
>,
res: MedusaResponse<HttpTypes.AdminProductResponse>
) => {
const { additional_data, ...update } = req.validatedBody
const { result } = await updateProductsWorkflow(req.scope).run({
input: {
selector: { id: req.params.id },
update: req.validatedBody,
update,
additional_data,
},
})

View File

@@ -7,7 +7,7 @@ import {
MedusaResponse,
} from "../../../../../../types/routing"
import { HttpTypes } from "@medusajs/types"
import { AdditionalData, HttpTypes } from "@medusajs/types"
import { refetchEntity } from "../../../../../utils/refetch-entity"
import {
remapKeysForProduct,
@@ -35,16 +35,20 @@ export const GET = async (
}
export const POST = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminUpdateProductVariant>,
req: AuthenticatedMedusaRequest<
HttpTypes.AdminUpdateProductVariant & AdditionalData
>,
res: MedusaResponse<HttpTypes.AdminProductResponse>
) => {
const productId = req.params.id
const variantId = req.params.variant_id
const { additional_data, ...update } = req.validatedBody
await updateProductVariantsWorkflow(req.scope).run({
input: {
selector: { id: variantId, product_id: productId },
update: req.validatedBody,
update: update,
additional_data,
},
})

View File

@@ -1,5 +1,5 @@
import { createProductVariantsWorkflow } from "@medusajs/core-flows"
import { HttpTypes } from "@medusajs/types"
import { AdditionalData, HttpTypes } from "@medusajs/types"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
@@ -53,19 +53,23 @@ export const GET = async (
}
export const POST = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminCreateProductVariant>,
req: AuthenticatedMedusaRequest<
HttpTypes.AdminCreateProductVariant & AdditionalData
>,
res: MedusaResponse<HttpTypes.AdminProductResponse>
) => {
const productId = req.params.id
const { additional_data, ...rest } = req.validatedBody
const input = [
{
...req.validatedBody,
...rest,
product_id: productId,
},
]
await createProductVariantsWorkflow(req.scope).run({
input: { product_variants: input },
input: { product_variants: input, additional_data },
})
const product = await refetchEntity(

View File

@@ -27,6 +27,8 @@ import {
AdminUpdateProductOption,
AdminUpdateProductVariant,
AdminUpdateVariantInventoryItem,
CreateProduct,
CreateProductVariant,
} from "./validators"
import multer from "multer"
@@ -68,7 +70,7 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
matcher: "/admin/products/batch",
middlewares: [
validateAndTransformBody(
createBatchBody(AdminCreateProduct, AdminBatchUpdateProduct)
createBatchBody(CreateProduct, AdminBatchUpdateProduct)
),
validateAndTransformQuery(
AdminGetProductParams,
@@ -166,10 +168,7 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
matcher: "/admin/products/:id/variants/batch",
middlewares: [
validateAndTransformBody(
createBatchBody(
AdminCreateProductVariant,
AdminBatchUpdateProductVariant
)
createBatchBody(CreateProductVariant, AdminBatchUpdateProductVariant)
),
validateAndTransformQuery(
AdminGetProductVariantParams,

View File

@@ -1,5 +1,5 @@
import { createProductsWorkflow } from "@medusajs/core-flows"
import { HttpTypes } from "@medusajs/types"
import { AdditionalData, HttpTypes } from "@medusajs/types"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
@@ -30,11 +30,15 @@ export const GET = async (
}
export const POST = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminCreateProduct>,
req: AuthenticatedMedusaRequest<
HttpTypes.AdminCreateProduct & AdditionalData
>,
res: MedusaResponse<HttpTypes.AdminProductResponse>
) => {
const { additional_data, ...products } = req.validatedBody
const { result } = await createProductsWorkflow(req.scope).run({
input: { products: [req.validatedBody] },
input: { products: [products], additional_data },
})
const product = await refetchEntity(

View File

@@ -1,10 +1,10 @@
import { BatchMethodRequest } from "@medusajs/types"
import { ProductStatus } from "@medusajs/utils"
import { z } from "zod"
import {
GetProductsParams,
transformProductParams,
} from "../../utils/common-validators"
import { z, ZodObject } from "zod"
import {
createFindParams,
createOperatorMap,
@@ -85,23 +85,46 @@ export const AdminUpdateProductTag = z.object({
value: z.string().optional(),
})
export type AdminCreateProductOptionType = z.infer<
typeof AdminCreateProductOption
>
export const AdminCreateProductOption = z.object({
export type AdminCreateProductOptionType = z.infer<typeof CreateProductOption>
export const CreateProductOption = z.object({
title: z.string(),
values: z.array(z.string()),
})
export const AdminCreateProductOption = (
additionalDataValidator?: ZodObject<any, any>
) => {
if (!additionalDataValidator) {
return CreateProductOption.extend({
additional_data: z.record(z.unknown()).nullish(),
})
}
export type AdminUpdateProductOptionType = z.infer<
typeof AdminUpdateProductOption
>
export const AdminUpdateProductOption = z.object({
return CreateProductOption.extend({
additional_data: additionalDataValidator,
})
}
export type AdminUpdateProductOptionType = z.infer<typeof UpdateProductOption>
export const UpdateProductOption = z.object({
id: z.string().optional(),
title: z.string().optional(),
values: z.array(z.string()).optional(),
})
export const AdminUpdateProductOption = (
additionalDataValidator?: ZodObject<any, any>
) => {
if (!additionalDataValidator) {
return UpdateProductOption.extend({
additional_data: z.record(z.unknown()).nullish(),
})
}
return UpdateProductOption.extend({
additional_data: additionalDataValidator,
})
}
export type AdminCreateVariantPriceType = z.infer<
typeof AdminCreateVariantPrice
>
@@ -130,10 +153,8 @@ export const AdminCreateProductType = z.object({
value: z.string(),
})
export type AdminCreateProductVariantType = z.infer<
typeof AdminCreateProductVariant
>
export const AdminCreateProductVariant = z
export type AdminCreateProductVariantType = z.infer<typeof CreateProductVariant>
export const CreateProductVariant = z
.object({
title: z.string(),
sku: z.string().nullish(),
@@ -164,11 +185,22 @@ export const AdminCreateProductVariant = z
.optional(),
})
.strict()
export const AdminCreateProductVariant = (
additionalDataValidator?: ZodObject<any, any>
) => {
if (!additionalDataValidator) {
return CreateProductVariant.extend({
additional_data: z.record(z.string()).optional(),
})
}
export type AdminUpdateProductVariantType = z.infer<
typeof AdminUpdateProductVariant
>
export const AdminUpdateProductVariant = z
return CreateProductVariant.extend({
additional_data: additionalDataValidator,
})
}
export type AdminUpdateProductVariantType = z.infer<typeof UpdateProductVariant>
export const UpdateProductVariant = z
.object({
id: z.string().optional(),
title: z.string().optional(),
@@ -193,10 +225,24 @@ export const AdminUpdateProductVariant = z
})
.strict()
export const AdminUpdateProductVariant = (
additionalDataValidator?: ZodObject<any, any>
) => {
if (!additionalDataValidator) {
return UpdateProductVariant.extend({
additional_data: z.record(z.string()).optional(),
})
}
return UpdateProductVariant.extend({
additional_data: additionalDataValidator,
})
}
export type AdminBatchUpdateProductVariantType = z.infer<
typeof AdminBatchUpdateProductVariant
>
export const AdminBatchUpdateProductVariant = AdminUpdateProductVariant.extend({
export const AdminBatchUpdateProductVariant = UpdateProductVariant.extend({
id: z.string(),
})
@@ -204,8 +250,8 @@ export const IdAssociation = z.object({
id: z.string(),
})
export type AdminCreateProductType = z.infer<typeof AdminCreateProduct>
export const AdminCreateProduct = z
export type AdminCreateProductType = z.infer<typeof CreateProduct>
export const CreateProduct = z
.object({
title: z.string(),
subtitle: z.string().nullish(),
@@ -220,8 +266,8 @@ export const AdminCreateProduct = z
collection_id: z.string().nullish(),
categories: z.array(IdAssociation).optional(),
tags: z.array(IdAssociation).optional(),
options: z.array(AdminCreateProductOption).optional(),
variants: z.array(AdminCreateProductVariant).optional(),
options: z.array(CreateProductOption).optional(),
variants: z.array(CreateProductVariant).optional(),
sales_channels: z.array(z.object({ id: z.string() })).optional(),
weight: z.number().nullish(),
length: z.number().nullish(),
@@ -235,14 +281,28 @@ export const AdminCreateProduct = z
})
.strict()
export type AdminUpdateProductType = z.infer<typeof AdminUpdateProduct>
export const AdminUpdateProduct = z
export const AdminCreateProduct = (
additionalDataValidator?: ZodObject<any, any>
) => {
if (!additionalDataValidator) {
return CreateProduct.extend({
additional_data: z.record(z.unknown()).nullish(),
})
}
return CreateProduct.extend({
additional_data: additionalDataValidator,
})
}
export type AdminUpdateProductType = z.infer<typeof UpdateProduct>
export const UpdateProduct = z
.object({
title: z.string().optional(),
discountable: z.boolean().optional(),
is_giftcard: z.boolean().optional(),
options: z.array(AdminUpdateProductOption).optional(),
variants: z.array(AdminUpdateProductVariant).optional(),
options: z.array(UpdateProductOption).optional(),
variants: z.array(UpdateProductVariant).optional(),
status: statusEnum.optional(),
subtitle: z.string().nullish(),
description: z.string().nullish(),
@@ -266,10 +326,24 @@ export const AdminUpdateProduct = z
})
.strict()
export const AdminUpdateProduct = (
additionalDataValidator?: ZodObject<any, any>
) => {
if (!additionalDataValidator) {
return UpdateProduct.extend({
additional_data: z.record(z.unknown()).nullish(),
})
}
return UpdateProduct.extend({
additional_data: additionalDataValidator,
})
}
export type AdminBatchUpdateProductType = z.infer<
typeof AdminBatchUpdateProduct
>
export const AdminBatchUpdateProduct = AdminUpdateProduct.extend({
export const AdminBatchUpdateProduct = UpdateProduct.extend({
id: z.string(),
})

View File

@@ -9,7 +9,7 @@ describe("validateAndTransformBody", () => {
jest.clearAllMocks()
})
it("should merge a custom validators schema", async () => {
it("should pass additionalDataValidator to validator factory", async () => {
let mockRequest = {
query: {},
body: {},
@@ -18,42 +18,16 @@ describe("validateAndTransformBody", () => {
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(),
}),
}
mockRequest.additionalDataValidator = 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(

View File

@@ -695,56 +695,4 @@ 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`
)
)
})
})

View File

@@ -17,12 +17,8 @@ export function validateAndTransformBody(
return async (req: MedusaRequest, _: MedusaResponse, next: NextFunction) => {
try {
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)
schema = zodSchema(req.additionalDataValidator)
} else {
schema = zodSchema
}

View File

@@ -58,11 +58,7 @@ const getFilterableFields = <T extends RequestQueryFields>(obj: T): T => {
}
export function validateAndTransformQuery<TEntity extends BaseEntity>(
zodSchema:
| z.ZodObject<any, any>
| ((
customSchema?: z.ZodObject<any, any>
) => z.ZodObject<any, any> | z.ZodEffects<any, any>),
zodSchema: z.ZodObject<any, any> | z.ZodEffects<any, any>,
queryConfig: QueryConfig<TEntity>
): (
req: MedusaRequest,
@@ -75,18 +71,7 @@ export function validateAndTransformQuery<TEntity extends BaseEntity>(
delete req.allowed
const query = normalizeQuery(req)
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 validated = await zodValidator(zodSchema, query)
const cnf = queryConfig.isList
? prepareListQuery(validated, { ...queryConfig, allowed })
: prepareRetrieveQuery(validated, { ...queryConfig, allowed })

View File

@@ -63,14 +63,12 @@ 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.
* Custom validator to validate the `additional_data` property in
* requests that allows for additional_data
*/
extendedValidators?: {
body?: ZodObject<any, any>
queryParams?: ZodObject<any, any>
}
additionalDataValidator?: ZodObject<any, any>
}
export interface AuthContext {

View File

@@ -21,22 +21,20 @@ describe("defineMiddlewares", function () {
})
})
test("should wrap body extendedValidator to middleware", () => {
test("should wrap additionalDataValidator to middleware", () => {
const req = {
body: {},
} as MedusaRequest
const res = {} as MedusaResponse
const nextFn = jest.fn()
const schema = zod.object({
const schema = {
brand_id: zod.string(),
})
}
const config = defineMiddlewares([
{
matcher: "/admin/products",
extendedValidators: {
body: schema,
},
additionalDataValidator: schema,
},
])
@@ -50,42 +48,10 @@ describe("defineMiddlewares", function () {
})
config.routes?.[0].middlewares?.[0](req, res, nextFn)
expect(req.extendedValidators).toMatchObject({
body: schema,
})
})
test("should wrap queryParams extendedValidator to middleware", () => {
const req = {
body: {},
} as MedusaRequest
const res = {} as MedusaResponse
const nextFn = jest.fn()
const schema = zod.object({
brand_id: zod.string(),
})
const config = defineMiddlewares([
expect(req.additionalDataValidator!.parse({ brand_id: "1" })).toMatchObject(
{
matcher: "/admin/products",
extendedValidators: {
queryParams: schema,
},
},
])
expect(config).toMatchObject({
routes: [
{
matcher: "/admin/products",
middlewares: [expect.any(Function)],
},
],
})
config.routes?.[0].middlewares?.[0](req, res, nextFn)
expect(req.extendedValidators).toMatchObject({
queryParams: schema,
})
brand_id: "1",
}
)
})
})