From 0706bab663a0d0d5e9a939720078a3cd3c0abc14 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 5 Aug 2024 09:24:49 +0530 Subject: [PATCH] feature: introduce additional_data to the product endpoints (#8405) --- .../product/workflows/create-collections.ts | 5 +- .../workflows/create-product-options.ts | 5 +- .../product/workflows/create-product-tags.ts | 5 +- .../product/workflows/create-product-types.ts | 5 +- .../workflows/create-product-variants.ts | 10 +- .../src/product/workflows/create-products.ts | 4 +- .../product/workflows/update-collections.ts | 5 +- .../workflows/update-product-options.ts | 5 +- .../product/workflows/update-product-tags.ts | 5 +- .../product/workflows/update-product-types.ts | 5 +- .../workflows/update-product-variants.ts | 6 +- .../src/product/workflows/update-products.ts | 7 +- .../types/src/http/common/additional_data.ts | 7 + packages/core/types/src/http/common/index.ts | 1 + .../framework/framework/src/http/types.ts | 10 +- .../src/http/utils/define-middlewares.ts | 18 +-- .../[id]/options/[option_id]/route.ts | 10 +- .../api/admin/products/[id]/options/route.ts | 11 +- .../src/api/admin/products/[id]/route.ts | 11 +- .../[id]/variants/[variant_id]/route.ts | 10 +- .../api/admin/products/[id]/variants/route.ts | 12 +- .../src/api/admin/products/middlewares.ts | 9 +- .../medusa/src/api/admin/products/route.ts | 10 +- .../src/api/admin/products/validators.ts | 128 ++++++++++++++---- .../api/utils/__tests__/validate-body.spec.ts | 36 +---- .../utils/__tests__/validate-query.spec.ts | 52 ------- .../medusa/src/api/utils/validate-body.ts | 6 +- .../medusa/src/api/utils/validate-query.ts | 19 +-- packages/medusa/src/types/routing.ts | 10 +- .../__tests__/define-routes-config.spec.ts | 50 ++----- 30 files changed, 219 insertions(+), 258 deletions(-) create mode 100644 packages/core/types/src/http/common/additional_data.ts diff --git a/packages/core/core-flows/src/product/workflows/create-collections.ts b/packages/core/core-flows/src/product/workflows/create-collections.ts index d05520eaf0..4773f7b607 100644 --- a/packages/core/core-flows/src/product/workflows/create-collections.ts +++ b/packages/core/core-flows/src/product/workflows/create-collections.ts @@ -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 -} +} & AdditionalData export const createCollectionsWorkflowId = "create-collections" export const createCollectionsWorkflow = createWorkflow( diff --git a/packages/core/core-flows/src/product/workflows/create-product-options.ts b/packages/core/core-flows/src/product/workflows/create-product-options.ts index 816c8a8642..da7828112b 100644 --- a/packages/core/core-flows/src/product/workflows/create-product-options.ts +++ b/packages/core/core-flows/src/product/workflows/create-product-options.ts @@ -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 -} +} & AdditionalData export const createProductOptionsWorkflowId = "create-product-options" diff --git a/packages/core/core-flows/src/product/workflows/create-product-tags.ts b/packages/core/core-flows/src/product/workflows/create-product-tags.ts index 7e1910f956..ac5660168c 100644 --- a/packages/core/core-flows/src/product/workflows/create-product-tags.ts +++ b/packages/core/core-flows/src/product/workflows/create-product-tags.ts @@ -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 -} +} & AdditionalData export const createProductTagsWorkflowId = "create-product-tags" diff --git a/packages/core/core-flows/src/product/workflows/create-product-types.ts b/packages/core/core-flows/src/product/workflows/create-product-types.ts index 1d0ea51f4e..65a41a9221 100644 --- a/packages/core/core-flows/src/product/workflows/create-product-types.ts +++ b/packages/core/core-flows/src/product/workflows/create-product-types.ts @@ -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 -} +} & AdditionalData export const createProductTypesWorkflowId = "create-product-types" diff --git a/packages/core/core-flows/src/product/workflows/create-product-variants.ts b/packages/core/core-flows/src/product/workflows/create-product-variants.ts index 554adbfada..2ebb6d1cd6 100644 --- a/packages/core/core-flows/src/product/workflows/create-product-variants.ts +++ b/packages/core/core-flows/src/product/workflows/create-product-variants.ts @@ -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 -} +} & AdditionalData const buildLink = ( variant_id: string, diff --git a/packages/core/core-flows/src/product/workflows/create-products.ts b/packages/core/core-flows/src/product/workflows/create-products.ts index 83601112b9..f7a5fe6d47 100644 --- a/packages/core/core-flows/src/product/workflows/create-products.ts +++ b/packages/core/core-flows/src/product/workflows/create-products.ts @@ -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 -} +} & AdditionalData export const createProductsWorkflowId = "create-products" diff --git a/packages/core/core-flows/src/product/workflows/update-collections.ts b/packages/core/core-flows/src/product/workflows/update-collections.ts index 900d4273a5..3336d4f9c7 100644 --- a/packages/core/core-flows/src/product/workflows/update-collections.ts +++ b/packages/core/core-flows/src/product/workflows/update-collections.ts @@ -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 -} +} & AdditionalData type WorkflowInput = UpdateCollectionsStepInput diff --git a/packages/core/core-flows/src/product/workflows/update-product-options.ts b/packages/core/core-flows/src/product/workflows/update-product-options.ts index 1ac16a4995..50e5563062 100644 --- a/packages/core/core-flows/src/product/workflows/update-product-options.ts +++ b/packages/core/core-flows/src/product/workflows/update-product-options.ts @@ -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 -} +} & AdditionalData type WorkflowInput = UpdateProductOptionsStepInput diff --git a/packages/core/core-flows/src/product/workflows/update-product-tags.ts b/packages/core/core-flows/src/product/workflows/update-product-tags.ts index 4fd67c0820..541af26b02 100644 --- a/packages/core/core-flows/src/product/workflows/update-product-tags.ts +++ b/packages/core/core-flows/src/product/workflows/update-product-tags.ts @@ -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 -} +} & AdditionalData type WorkflowInput = UpdateProductTagsStepInput diff --git a/packages/core/core-flows/src/product/workflows/update-product-types.ts b/packages/core/core-flows/src/product/workflows/update-product-types.ts index 3cfc55c447..278f90d45f 100644 --- a/packages/core/core-flows/src/product/workflows/update-product-types.ts +++ b/packages/core/core-flows/src/product/workflows/update-product-types.ts @@ -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 -} +} & AdditionalData type WorkflowInput = UpdateProductTypesStepInput diff --git a/packages/core/core-flows/src/product/workflows/update-product-variants.ts b/packages/core/core-flows/src/product/workflows/update-product-variants.ts index 44724db92a..3324a1da5c 100644 --- a/packages/core/core-flows/src/product/workflows/update-product-variants.ts +++ b/packages/core/core-flows/src/product/workflows/update-product-variants.ts @@ -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 -} +type WorkflowInput = UpdateProductVariantsStepInput & AdditionalData export const updateProductVariantsWorkflowId = "update-product-variants" export const updateProductVariantsWorkflow = createWorkflow( diff --git a/packages/core/core-flows/src/product/workflows/update-products.ts b/packages/core/core-flows/src/product/workflows/update-products.ts index e0971cf96b..6d0aec3849 100644 --- a/packages/core/core-flows/src/product/workflows/update-products.ts +++ b/packages/core/core-flows/src/product/workflows/update-products.ts @@ -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 -} +} & AdditionalData type UpdateProductsStepInputProducts = { products: (Omit & { sales_channels?: { id: string }[] variants?: UpdateProductVariantWorkflowInputDTO[] })[] - additional_data?: Record -} +} & AdditionalData type UpdateProductsStepInput = | UpdateProductsStepInputSelector diff --git a/packages/core/types/src/http/common/additional_data.ts b/packages/core/types/src/http/common/additional_data.ts new file mode 100644 index 0000000000..b0ae423ec8 --- /dev/null +++ b/packages/core/types/src/http/common/additional_data.ts @@ -0,0 +1,7 @@ +/** + * Represents the additional_data property accepted in HTTP + * requests to allow arbitrary values + */ +export type AdditionalData = { + additional_data?: Record +} diff --git a/packages/core/types/src/http/common/index.ts b/packages/core/types/src/http/common/index.ts index 64c6d7ab23..47a302714d 100644 --- a/packages/core/types/src/http/common/index.ts +++ b/packages/core/types/src/http/common/index.ts @@ -1,2 +1,3 @@ export * from "./request" export * from "./response" +export * from "./additional_data" diff --git a/packages/framework/framework/src/http/types.ts b/packages/framework/framework/src/http/types.ts index f632d26079..50c4a35b94 100644 --- a/packages/framework/framework/src/http/types.ts +++ b/packages/framework/framework/src/http/types.ts @@ -147,14 +147,12 @@ 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. + * Custom validator to validate the `additional_data` property in + * requests that allows for additional_data */ - extendedValidators?: { - body?: ZodObject - queryParams?: ZodObject - } + additionalDataValidator?: ZodObject } export interface AuthContext { diff --git a/packages/framework/framework/src/http/utils/define-middlewares.ts b/packages/framework/framework/src/http/utils/define-middlewares.ts index 61af0e4bff..5c080863b6 100644 --- a/packages/framework/framework/src/http/utils/define-middlewares.ts +++ b/packages/framework/framework/src/http/utils/define-middlewares.ts @@ -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 - queryParams?: ZodObject - } + additionalDataValidator?: ZodRawShape // eslint-disable-next-line space-before-function-paren middlewares?: (( 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() }) } diff --git a/packages/medusa/src/api/admin/products/[id]/options/[option_id]/route.ts b/packages/medusa/src/api/admin/products/[id]/options/[option_id]/route.ts index 92beb1cd30..9d11100f1f 100644 --- a/packages/medusa/src/api/admin/products/[id]/options/[option_id]/route.ts +++ b/packages/medusa/src/api/admin/products/[id]/options/[option_id]/route.ts @@ -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, + req: AuthenticatedMedusaRequest< + HttpTypes.AdminUpdateProductOption & AdditionalData + >, res: MedusaResponse ) => { 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, }, }) diff --git a/packages/medusa/src/api/admin/products/[id]/options/route.ts b/packages/medusa/src/api/admin/products/[id]/options/route.ts index e91d7f6fc3..68d3e1a527 100644 --- a/packages/medusa/src/api/admin/products/[id]/options/route.ts +++ b/packages/medusa/src/api/admin/products/[id]/options/route.ts @@ -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, + req: AuthenticatedMedusaRequest< + HttpTypes.AdminCreateProductOption & AdditionalData + >, res: MedusaResponse ) => { 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, }, }) diff --git a/packages/medusa/src/api/admin/products/[id]/route.ts b/packages/medusa/src/api/admin/products/[id]/route.ts index b79124641d..d69c831328 100644 --- a/packages/medusa/src/api/admin/products/[id]/route.ts +++ b/packages/medusa/src/api/admin/products/[id]/route.ts @@ -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, + req: AuthenticatedMedusaRequest< + HttpTypes.AdminUpdateProduct & AdditionalData + >, res: MedusaResponse ) => { + 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, }, }) diff --git a/packages/medusa/src/api/admin/products/[id]/variants/[variant_id]/route.ts b/packages/medusa/src/api/admin/products/[id]/variants/[variant_id]/route.ts index 06791cb42b..296da43b07 100644 --- a/packages/medusa/src/api/admin/products/[id]/variants/[variant_id]/route.ts +++ b/packages/medusa/src/api/admin/products/[id]/variants/[variant_id]/route.ts @@ -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, + req: AuthenticatedMedusaRequest< + HttpTypes.AdminUpdateProductVariant & AdditionalData + >, res: MedusaResponse ) => { 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, }, }) diff --git a/packages/medusa/src/api/admin/products/[id]/variants/route.ts b/packages/medusa/src/api/admin/products/[id]/variants/route.ts index f99abed49c..3543db3796 100644 --- a/packages/medusa/src/api/admin/products/[id]/variants/route.ts +++ b/packages/medusa/src/api/admin/products/[id]/variants/route.ts @@ -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, + req: AuthenticatedMedusaRequest< + HttpTypes.AdminCreateProductVariant & AdditionalData + >, res: MedusaResponse ) => { 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( diff --git a/packages/medusa/src/api/admin/products/middlewares.ts b/packages/medusa/src/api/admin/products/middlewares.ts index cab1dea7a4..9d8d315f31 100644 --- a/packages/medusa/src/api/admin/products/middlewares.ts +++ b/packages/medusa/src/api/admin/products/middlewares.ts @@ -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, diff --git a/packages/medusa/src/api/admin/products/route.ts b/packages/medusa/src/api/admin/products/route.ts index 04fb40cbd7..79245cf909 100644 --- a/packages/medusa/src/api/admin/products/route.ts +++ b/packages/medusa/src/api/admin/products/route.ts @@ -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, + req: AuthenticatedMedusaRequest< + HttpTypes.AdminCreateProduct & AdditionalData + >, res: MedusaResponse ) => { + 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( diff --git a/packages/medusa/src/api/admin/products/validators.ts b/packages/medusa/src/api/admin/products/validators.ts index 361ea59e77..6eb2ed9315 100644 --- a/packages/medusa/src/api/admin/products/validators.ts +++ b/packages/medusa/src/api/admin/products/validators.ts @@ -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 +export const CreateProductOption = z.object({ title: z.string(), values: z.array(z.string()), }) +export const AdminCreateProductOption = ( + additionalDataValidator?: ZodObject +) => { + 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 +export const UpdateProductOption = z.object({ id: z.string().optional(), title: z.string().optional(), values: z.array(z.string()).optional(), }) +export const AdminUpdateProductOption = ( + additionalDataValidator?: ZodObject +) => { + 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 +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 +) => { + 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 +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 +) => { + 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 -export const AdminCreateProduct = z +export type AdminCreateProductType = z.infer +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 -export const AdminUpdateProduct = z +export const AdminCreateProduct = ( + additionalDataValidator?: ZodObject +) => { + if (!additionalDataValidator) { + return CreateProduct.extend({ + additional_data: z.record(z.unknown()).nullish(), + }) + } + + return CreateProduct.extend({ + additional_data: additionalDataValidator, + }) +} + +export type AdminUpdateProductType = z.infer +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 +) => { + 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(), }) diff --git a/packages/medusa/src/api/utils/__tests__/validate-body.spec.ts b/packages/medusa/src/api/utils/__tests__/validate-body.spec.ts index 0706172a76..2b6e712280 100644 --- a/packages/medusa/src/api/utils/__tests__/validate-body.spec.ts +++ b/packages/medusa/src/api/utils/__tests__/validate-body.spec.ts @@ -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) => { return schema ? createLinkBody().merge(schema) : createLinkBody() } let middleware = validateAndTransformBody(validatorFactory) + await middleware(mockRequest, mockResponse, nextFunction) expect(nextFunction).toHaveBeenCalledWith( new MedusaError( 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 b804473c6c..fe9ea099bf 100644 --- a/packages/medusa/src/api/utils/__tests__/validate-query.spec.ts +++ b/packages/medusa/src/api/utils/__tests__/validate-query.spec.ts @@ -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) => { - 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 af4186f386..8512228175 100644 --- a/packages/medusa/src/api/utils/validate-body.ts +++ b/packages/medusa/src/api/utils/validate-body.ts @@ -17,12 +17,8 @@ export function validateAndTransformBody( return async (req: MedusaRequest, _: MedusaResponse, next: NextFunction) => { try { 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) + schema = zodSchema(req.additionalDataValidator) } else { schema = zodSchema } diff --git a/packages/medusa/src/api/utils/validate-query.ts b/packages/medusa/src/api/utils/validate-query.ts index e34b335023..434aeabb09 100644 --- a/packages/medusa/src/api/utils/validate-query.ts +++ b/packages/medusa/src/api/utils/validate-query.ts @@ -58,11 +58,7 @@ const getFilterableFields = (obj: T): T => { } export function validateAndTransformQuery( - zodSchema: - | z.ZodObject - | (( - customSchema?: z.ZodObject - ) => z.ZodObject | z.ZodEffects), + zodSchema: z.ZodObject | z.ZodEffects, queryConfig: QueryConfig ): ( req: MedusaRequest, @@ -75,18 +71,7 @@ export function validateAndTransformQuery( delete req.allowed const query = normalizeQuery(req) - 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 validated = await zodValidator(zodSchema, 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 5c6420d8a2..74a4de9d09 100644 --- a/packages/medusa/src/types/routing.ts +++ b/packages/medusa/src/types/routing.ts @@ -63,14 +63,12 @@ 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. + * Custom validator to validate the `additional_data` property in + * requests that allows for additional_data */ - extendedValidators?: { - body?: ZodObject - queryParams?: ZodObject - } + additionalDataValidator?: ZodObject } export interface AuthContext { diff --git a/packages/medusa/src/utils/__tests__/define-routes-config.spec.ts b/packages/medusa/src/utils/__tests__/define-routes-config.spec.ts index 632020d4d8..bf3e4d94fc 100644 --- a/packages/medusa/src/utils/__tests__/define-routes-config.spec.ts +++ b/packages/medusa/src/utils/__tests__/define-routes-config.spec.ts @@ -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", + } + ) }) })