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
@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types" import { AdditionalData, ProductTypes } from "@medusajs/types"
import { import {
WorkflowData, WorkflowData,
WorkflowResponse, WorkflowResponse,
@@ -9,8 +9,7 @@ import { createCollectionsStep } from "../steps"
type WorkflowInput = { type WorkflowInput = {
collections: ProductTypes.CreateProductCollectionDTO[] collections: ProductTypes.CreateProductCollectionDTO[]
additional_data?: Record<string, unknown> } & AdditionalData
}
export const createCollectionsWorkflowId = "create-collections" export const createCollectionsWorkflowId = "create-collections"
export const createCollectionsWorkflow = createWorkflow( export const createCollectionsWorkflow = createWorkflow(
@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types" import { AdditionalData, ProductTypes } from "@medusajs/types"
import { import {
WorkflowData, WorkflowData,
WorkflowResponse, WorkflowResponse,
@@ -9,8 +9,7 @@ import { createProductOptionsStep } from "../steps"
type WorkflowInput = { type WorkflowInput = {
product_options: ProductTypes.CreateProductOptionDTO[] product_options: ProductTypes.CreateProductOptionDTO[]
additional_data?: Record<string, unknown> } & AdditionalData
}
export const createProductOptionsWorkflowId = "create-product-options" export const createProductOptionsWorkflowId = "create-product-options"
@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types" import { AdditionalData, ProductTypes } from "@medusajs/types"
import { import {
WorkflowData, WorkflowData,
WorkflowResponse, WorkflowResponse,
@@ -9,8 +9,7 @@ import { createProductTagsStep } from "../steps"
type WorkflowInput = { type WorkflowInput = {
product_tags: ProductTypes.CreateProductTagDTO[] product_tags: ProductTypes.CreateProductTagDTO[]
additional_data?: Record<string, unknown> } & AdditionalData
}
export const createProductTagsWorkflowId = "create-product-tags" export const createProductTagsWorkflowId = "create-product-tags"
@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types" import { AdditionalData, ProductTypes } from "@medusajs/types"
import { import {
WorkflowData, WorkflowData,
WorkflowResponse, WorkflowResponse,
@@ -9,8 +9,7 @@ import { createProductTypesStep } from "../steps"
type WorkflowInput = { type WorkflowInput = {
product_types: ProductTypes.CreateProductTypeDTO[] product_types: ProductTypes.CreateProductTypeDTO[]
additional_data?: Record<string, unknown> } & AdditionalData
}
export const createProductTypesWorkflowId = "create-product-types" export const createProductTypesWorkflowId = "create-product-types"
@@ -1,5 +1,10 @@
import { LinkDefinition } from "@medusajs/modules-sdk" import { LinkDefinition } from "@medusajs/modules-sdk"
import { InventoryTypes, PricingTypes, ProductTypes } from "@medusajs/types" import {
AdditionalData,
InventoryTypes,
PricingTypes,
ProductTypes,
} from "@medusajs/types"
import { import {
WorkflowData, WorkflowData,
WorkflowResponse, WorkflowResponse,
@@ -25,8 +30,7 @@ type WorkflowInput = {
required_quantity?: number required_quantity?: number
}[] }[]
})[] })[]
additional_data?: Record<string, unknown> } & AdditionalData
}
const buildLink = ( const buildLink = (
variant_id: string, variant_id: string,
@@ -1,4 +1,5 @@
import { import {
AdditionalData,
CreateProductWorkflowInputDTO, CreateProductWorkflowInputDTO,
PricingTypes, PricingTypes,
ProductTypes, ProductTypes,
@@ -17,8 +18,7 @@ import { createProductVariantsWorkflow } from "./create-product-variants"
type WorkflowInput = { type WorkflowInput = {
products: CreateProductWorkflowInputDTO[] products: CreateProductWorkflowInputDTO[]
additional_data?: Record<string, unknown> } & AdditionalData
}
export const createProductsWorkflowId = "create-products" export const createProductsWorkflowId = "create-products"
@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types" import { AdditionalData, ProductTypes } from "@medusajs/types"
import { import {
WorkflowData, WorkflowData,
WorkflowResponse, WorkflowResponse,
@@ -10,8 +10,7 @@ import { updateCollectionsStep } from "../steps"
type UpdateCollectionsStepInput = { type UpdateCollectionsStepInput = {
selector: ProductTypes.FilterableProductCollectionProps selector: ProductTypes.FilterableProductCollectionProps
update: ProductTypes.UpdateProductCollectionDTO update: ProductTypes.UpdateProductCollectionDTO
additional_data?: Record<string, unknown> } & AdditionalData
}
type WorkflowInput = UpdateCollectionsStepInput type WorkflowInput = UpdateCollectionsStepInput
@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types" import { AdditionalData, ProductTypes } from "@medusajs/types"
import { import {
WorkflowData, WorkflowData,
WorkflowResponse, WorkflowResponse,
@@ -10,8 +10,7 @@ import { updateProductOptionsStep } from "../steps"
type UpdateProductOptionsStepInput = { type UpdateProductOptionsStepInput = {
selector: ProductTypes.FilterableProductOptionProps selector: ProductTypes.FilterableProductOptionProps
update: ProductTypes.UpdateProductOptionDTO update: ProductTypes.UpdateProductOptionDTO
additional_data?: Record<string, unknown> } & AdditionalData
}
type WorkflowInput = UpdateProductOptionsStepInput type WorkflowInput = UpdateProductOptionsStepInput
@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types" import { AdditionalData, ProductTypes } from "@medusajs/types"
import { import {
WorkflowData, WorkflowData,
WorkflowResponse, WorkflowResponse,
@@ -10,8 +10,7 @@ import { updateProductTagsStep } from "../steps"
type UpdateProductTagsStepInput = { type UpdateProductTagsStepInput = {
selector: ProductTypes.FilterableProductTypeProps selector: ProductTypes.FilterableProductTypeProps
update: ProductTypes.UpdateProductTypeDTO update: ProductTypes.UpdateProductTypeDTO
additional_data?: Record<string, unknown> } & AdditionalData
}
type WorkflowInput = UpdateProductTagsStepInput type WorkflowInput = UpdateProductTagsStepInput
@@ -1,4 +1,4 @@
import { ProductTypes } from "@medusajs/types" import { AdditionalData, ProductTypes } from "@medusajs/types"
import { import {
WorkflowData, WorkflowData,
WorkflowResponse, WorkflowResponse,
@@ -10,8 +10,7 @@ import { updateProductTypesStep } from "../steps"
type UpdateProductTypesStepInput = { type UpdateProductTypesStepInput = {
selector: ProductTypes.FilterableProductTypeProps selector: ProductTypes.FilterableProductTypeProps
update: ProductTypes.UpdateProductTypeDTO update: ProductTypes.UpdateProductTypeDTO
additional_data?: Record<string, unknown> } & AdditionalData
}
type WorkflowInput = UpdateProductTypesStepInput type WorkflowInput = UpdateProductTypesStepInput
@@ -1,4 +1,4 @@
import { PricingTypes, ProductTypes } from "@medusajs/types" import { AdditionalData, PricingTypes, ProductTypes } from "@medusajs/types"
import { import {
WorkflowData, WorkflowData,
WorkflowResponse, WorkflowResponse,
@@ -23,9 +23,7 @@ type UpdateProductVariantsStepInput =
})[] })[]
} }
type WorkflowInput = UpdateProductVariantsStepInput & { type WorkflowInput = UpdateProductVariantsStepInput & AdditionalData
additional_data?: Record<string, unknown>
}
export const updateProductVariantsWorkflowId = "update-product-variants" export const updateProductVariantsWorkflowId = "update-product-variants"
export const updateProductVariantsWorkflow = createWorkflow( export const updateProductVariantsWorkflow = createWorkflow(
@@ -1,6 +1,7 @@
import { updateProductsStep } from "../steps/update-products" import { updateProductsStep } from "../steps/update-products"
import { import {
AdditionalData,
CreateMoneyAmountDTO, CreateMoneyAmountDTO,
ProductTypes, ProductTypes,
UpdateProductVariantWorkflowInputDTO, UpdateProductVariantWorkflowInputDTO,
@@ -26,16 +27,14 @@ type UpdateProductsStepInputSelector = {
sales_channels?: { id: string }[] sales_channels?: { id: string }[]
variants?: UpdateProductVariantWorkflowInputDTO[] variants?: UpdateProductVariantWorkflowInputDTO[]
} }
additional_data?: Record<string, unknown> } & AdditionalData
}
type UpdateProductsStepInputProducts = { type UpdateProductsStepInputProducts = {
products: (Omit<ProductTypes.UpsertProductDTO, "variants"> & { products: (Omit<ProductTypes.UpsertProductDTO, "variants"> & {
sales_channels?: { id: string }[] sales_channels?: { id: string }[]
variants?: UpdateProductVariantWorkflowInputDTO[] variants?: UpdateProductVariantWorkflowInputDTO[]
})[] })[]
additional_data?: Record<string, unknown> } & AdditionalData
}
type UpdateProductsStepInput = type UpdateProductsStepInput =
| UpdateProductsStepInputSelector | UpdateProductsStepInputSelector
@@ -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>
}
@@ -1,2 +1,3 @@
export * from "./request" export * from "./request"
export * from "./response" export * from "./response"
export * from "./additional_data"
@@ -147,14 +147,12 @@ 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 * Custom validator to validate the `additional_data` property in
* merged with the original validator of the route. * requests that allows for additional_data
*/ */
extendedValidators?: { additionalDataValidator?: ZodObject<any, any>
body?: ZodObject<any, any>
queryParams?: ZodObject<any, any>
}
} }
export interface AuthContext { export interface AuthContext {
@@ -7,7 +7,7 @@ import {
MiddlewareVerb, MiddlewareVerb,
ParserConfig, ParserConfig,
} from "../types" } from "../types"
import { ZodObject } from "zod" import zod, { ZodRawShape } from "zod"
/** /**
* A helper function to configure the routes by defining custom middleware, * A helper function to configure the routes by defining custom middleware,
@@ -19,10 +19,7 @@ export function defineMiddlewares<
method?: MiddlewareVerb | MiddlewareVerb[] method?: MiddlewareVerb | MiddlewareVerb[]
matcher: string | RegExp matcher: string | RegExp
bodyParser?: ParserConfig bodyParser?: ParserConfig
extendedValidators?: { additionalDataValidator?: ZodRawShape
body?: ZodObject<any, any>
queryParams?: ZodObject<any, any>
}
// eslint-disable-next-line space-before-function-paren // eslint-disable-next-line space-before-function-paren
middlewares?: (<Req extends MedusaRequest>( middlewares?: (<Req extends MedusaRequest>(
req: Req, req: Req,
@@ -41,17 +38,16 @@ export function defineMiddlewares<
return { return {
errorHandler, errorHandler,
routes: routes.map((route) => { routes: routes.map((route) => {
const { middlewares, extendedValidators, ...rest } = route const { middlewares, additionalDataValidator, ...rest } = route
const customMiddleware: MedusaRequestHandler[] = [] const customMiddleware: MedusaRequestHandler[] = []
/** /**
* Define a custom validator when "extendedValidators.body" or * Define a custom validator when a zod schema is provided via
* "extendedValidators.queryParams" validation schema is * "additionalDataValidator" property
* provided.
*/ */
if (extendedValidators?.body || extendedValidators?.queryParams) { if (additionalDataValidator) {
customMiddleware.push((req, _, next) => { customMiddleware.push((req, _, next) => {
req.extendedValidators = extendedValidators req.additionalDataValidator = zod.object(additionalDataValidator)
next() next()
}) })
} }
@@ -8,7 +8,7 @@ import {
} from "@medusajs/core-flows" } from "@medusajs/core-flows"
import { remapKeysForProduct, remapProductResponse } from "../../../helpers" import { remapKeysForProduct, remapProductResponse } from "../../../helpers"
import { HttpTypes } from "@medusajs/types" import { AdditionalData, HttpTypes } from "@medusajs/types"
import { refetchEntity } from "../../../../../utils/refetch-entity" import { refetchEntity } from "../../../../../utils/refetch-entity"
export const GET = async ( export const GET = async (
@@ -28,16 +28,20 @@ export const GET = async (
} }
export const POST = async ( export const POST = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminUpdateProductOption>, req: AuthenticatedMedusaRequest<
HttpTypes.AdminUpdateProductOption & AdditionalData
>,
res: MedusaResponse<HttpTypes.AdminProductResponse> res: MedusaResponse<HttpTypes.AdminProductResponse>
) => { ) => {
const productId = req.params.id const productId = req.params.id
const optionId = req.params.option_id const optionId = req.params.option_id
const { additional_data, ...update } = req.validatedBody
await updateProductOptionsWorkflow(req.scope).run({ await updateProductOptionsWorkflow(req.scope).run({
input: { input: {
selector: { id: optionId, product_id: productId }, selector: { id: optionId, product_id: productId },
update: req.validatedBody, update,
additional_data,
}, },
}) })
@@ -5,7 +5,7 @@ import {
import { createProductOptionsWorkflow } from "@medusajs/core-flows" import { createProductOptionsWorkflow } from "@medusajs/core-flows"
import { remapKeysForProduct, remapProductResponse } from "../../helpers" import { remapKeysForProduct, remapProductResponse } from "../../helpers"
import { HttpTypes } from "@medusajs/types" import { AdditionalData, HttpTypes } from "@medusajs/types"
import { import {
refetchEntities, refetchEntities,
refetchEntity, refetchEntity,
@@ -33,18 +33,23 @@ export const GET = async (
} }
export const POST = async ( export const POST = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminCreateProductOption>, req: AuthenticatedMedusaRequest<
HttpTypes.AdminCreateProductOption & AdditionalData
>,
res: MedusaResponse<HttpTypes.AdminProductResponse> res: MedusaResponse<HttpTypes.AdminProductResponse>
) => { ) => {
const productId = req.params.id const productId = req.params.id
const { additional_data, ...rest } = req.validatedBody
await createProductOptionsWorkflow(req.scope).run({ await createProductOptionsWorkflow(req.scope).run({
input: { input: {
product_options: [ product_options: [
{ {
...req.validatedBody, ...rest,
product_id: productId, product_id: productId,
}, },
], ],
additional_data,
}, },
}) })
@@ -8,7 +8,7 @@ import {
} from "../../../../types/routing" } from "../../../../types/routing"
import { remapKeysForProduct, remapProductResponse } from "../helpers" import { remapKeysForProduct, remapProductResponse } from "../helpers"
import { MedusaError } from "@medusajs/utils" import { MedusaError } from "@medusajs/utils"
import { HttpTypes } from "@medusajs/types" import { AdditionalData, HttpTypes } from "@medusajs/types"
import { refetchEntity } from "../../../utils/refetch-entity" import { refetchEntity } from "../../../utils/refetch-entity"
export const GET = async ( export const GET = async (
@@ -31,13 +31,18 @@ export const GET = async (
} }
export const POST = async ( export const POST = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminUpdateProduct>, req: AuthenticatedMedusaRequest<
HttpTypes.AdminUpdateProduct & AdditionalData
>,
res: MedusaResponse<HttpTypes.AdminProductResponse> res: MedusaResponse<HttpTypes.AdminProductResponse>
) => { ) => {
const { additional_data, ...update } = req.validatedBody
const { result } = await updateProductsWorkflow(req.scope).run({ const { result } = await updateProductsWorkflow(req.scope).run({
input: { input: {
selector: { id: req.params.id }, selector: { id: req.params.id },
update: req.validatedBody, update,
additional_data,
}, },
}) })
@@ -7,7 +7,7 @@ import {
MedusaResponse, MedusaResponse,
} from "../../../../../../types/routing" } from "../../../../../../types/routing"
import { HttpTypes } from "@medusajs/types" import { AdditionalData, HttpTypes } from "@medusajs/types"
import { refetchEntity } from "../../../../../utils/refetch-entity" import { refetchEntity } from "../../../../../utils/refetch-entity"
import { import {
remapKeysForProduct, remapKeysForProduct,
@@ -35,16 +35,20 @@ export const GET = async (
} }
export const POST = async ( export const POST = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminUpdateProductVariant>, req: AuthenticatedMedusaRequest<
HttpTypes.AdminUpdateProductVariant & AdditionalData
>,
res: MedusaResponse<HttpTypes.AdminProductResponse> res: MedusaResponse<HttpTypes.AdminProductResponse>
) => { ) => {
const productId = req.params.id const productId = req.params.id
const variantId = req.params.variant_id const variantId = req.params.variant_id
const { additional_data, ...update } = req.validatedBody
await updateProductVariantsWorkflow(req.scope).run({ await updateProductVariantsWorkflow(req.scope).run({
input: { input: {
selector: { id: variantId, product_id: productId }, selector: { id: variantId, product_id: productId },
update: req.validatedBody, update: update,
additional_data,
}, },
}) })
@@ -1,5 +1,5 @@
import { createProductVariantsWorkflow } from "@medusajs/core-flows" import { createProductVariantsWorkflow } from "@medusajs/core-flows"
import { HttpTypes } from "@medusajs/types" import { AdditionalData, HttpTypes } from "@medusajs/types"
import { import {
AuthenticatedMedusaRequest, AuthenticatedMedusaRequest,
MedusaResponse, MedusaResponse,
@@ -53,19 +53,23 @@ export const GET = async (
} }
export const POST = async ( export const POST = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminCreateProductVariant>, req: AuthenticatedMedusaRequest<
HttpTypes.AdminCreateProductVariant & AdditionalData
>,
res: MedusaResponse<HttpTypes.AdminProductResponse> res: MedusaResponse<HttpTypes.AdminProductResponse>
) => { ) => {
const productId = req.params.id const productId = req.params.id
const { additional_data, ...rest } = req.validatedBody
const input = [ const input = [
{ {
...req.validatedBody, ...rest,
product_id: productId, product_id: productId,
}, },
] ]
await createProductVariantsWorkflow(req.scope).run({ await createProductVariantsWorkflow(req.scope).run({
input: { product_variants: input }, input: { product_variants: input, additional_data },
}) })
const product = await refetchEntity( const product = await refetchEntity(
@@ -27,6 +27,8 @@ import {
AdminUpdateProductOption, AdminUpdateProductOption,
AdminUpdateProductVariant, AdminUpdateProductVariant,
AdminUpdateVariantInventoryItem, AdminUpdateVariantInventoryItem,
CreateProduct,
CreateProductVariant,
} from "./validators" } from "./validators"
import multer from "multer" import multer from "multer"
@@ -68,7 +70,7 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
matcher: "/admin/products/batch", matcher: "/admin/products/batch",
middlewares: [ middlewares: [
validateAndTransformBody( validateAndTransformBody(
createBatchBody(AdminCreateProduct, AdminBatchUpdateProduct) createBatchBody(CreateProduct, AdminBatchUpdateProduct)
), ),
validateAndTransformQuery( validateAndTransformQuery(
AdminGetProductParams, AdminGetProductParams,
@@ -166,10 +168,7 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
matcher: "/admin/products/:id/variants/batch", matcher: "/admin/products/:id/variants/batch",
middlewares: [ middlewares: [
validateAndTransformBody( validateAndTransformBody(
createBatchBody( createBatchBody(CreateProductVariant, AdminBatchUpdateProductVariant)
AdminCreateProductVariant,
AdminBatchUpdateProductVariant
)
), ),
validateAndTransformQuery( validateAndTransformQuery(
AdminGetProductVariantParams, AdminGetProductVariantParams,
@@ -1,5 +1,5 @@
import { createProductsWorkflow } from "@medusajs/core-flows" import { createProductsWorkflow } from "@medusajs/core-flows"
import { HttpTypes } from "@medusajs/types" import { AdditionalData, HttpTypes } from "@medusajs/types"
import { import {
AuthenticatedMedusaRequest, AuthenticatedMedusaRequest,
MedusaResponse, MedusaResponse,
@@ -30,11 +30,15 @@ export const GET = async (
} }
export const POST = async ( export const POST = async (
req: AuthenticatedMedusaRequest<HttpTypes.AdminCreateProduct>, req: AuthenticatedMedusaRequest<
HttpTypes.AdminCreateProduct & AdditionalData
>,
res: MedusaResponse<HttpTypes.AdminProductResponse> res: MedusaResponse<HttpTypes.AdminProductResponse>
) => { ) => {
const { additional_data, ...products } = req.validatedBody
const { result } = await createProductsWorkflow(req.scope).run({ const { result } = await createProductsWorkflow(req.scope).run({
input: { products: [req.validatedBody] }, input: { products: [products], additional_data },
}) })
const product = await refetchEntity( const product = await refetchEntity(
@@ -1,10 +1,10 @@
import { BatchMethodRequest } from "@medusajs/types" import { BatchMethodRequest } from "@medusajs/types"
import { ProductStatus } from "@medusajs/utils" import { ProductStatus } from "@medusajs/utils"
import { z } from "zod"
import { import {
GetProductsParams, GetProductsParams,
transformProductParams, transformProductParams,
} from "../../utils/common-validators" } from "../../utils/common-validators"
import { z, ZodObject } from "zod"
import { import {
createFindParams, createFindParams,
createOperatorMap, createOperatorMap,
@@ -85,23 +85,46 @@ export const AdminUpdateProductTag = z.object({
value: z.string().optional(), value: z.string().optional(),
}) })
export type AdminCreateProductOptionType = z.infer< export type AdminCreateProductOptionType = z.infer<typeof CreateProductOption>
typeof AdminCreateProductOption export const CreateProductOption = z.object({
>
export const AdminCreateProductOption = z.object({
title: z.string(), title: z.string(),
values: z.array(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< return CreateProductOption.extend({
typeof AdminUpdateProductOption additional_data: additionalDataValidator,
> })
export const AdminUpdateProductOption = z.object({ }
export type AdminUpdateProductOptionType = z.infer<typeof UpdateProductOption>
export const UpdateProductOption = z.object({
id: z.string().optional(), id: z.string().optional(),
title: z.string().optional(), title: z.string().optional(),
values: z.array(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< export type AdminCreateVariantPriceType = z.infer<
typeof AdminCreateVariantPrice typeof AdminCreateVariantPrice
> >
@@ -130,10 +153,8 @@ export const AdminCreateProductType = z.object({
value: z.string(), value: z.string(),
}) })
export type AdminCreateProductVariantType = z.infer< export type AdminCreateProductVariantType = z.infer<typeof CreateProductVariant>
typeof AdminCreateProductVariant export const CreateProductVariant = z
>
export const AdminCreateProductVariant = z
.object({ .object({
title: z.string(), title: z.string(),
sku: z.string().nullish(), sku: z.string().nullish(),
@@ -164,11 +185,22 @@ export const AdminCreateProductVariant = z
.optional(), .optional(),
}) })
.strict() .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< return CreateProductVariant.extend({
typeof AdminUpdateProductVariant additional_data: additionalDataValidator,
> })
export const AdminUpdateProductVariant = z }
export type AdminUpdateProductVariantType = z.infer<typeof UpdateProductVariant>
export const UpdateProductVariant = z
.object({ .object({
id: z.string().optional(), id: z.string().optional(),
title: z.string().optional(), title: z.string().optional(),
@@ -193,10 +225,24 @@ export const AdminUpdateProductVariant = z
}) })
.strict() .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< export type AdminBatchUpdateProductVariantType = z.infer<
typeof AdminBatchUpdateProductVariant typeof AdminBatchUpdateProductVariant
> >
export const AdminBatchUpdateProductVariant = AdminUpdateProductVariant.extend({ export const AdminBatchUpdateProductVariant = UpdateProductVariant.extend({
id: z.string(), id: z.string(),
}) })
@@ -204,8 +250,8 @@ export const IdAssociation = z.object({
id: z.string(), id: z.string(),
}) })
export type AdminCreateProductType = z.infer<typeof AdminCreateProduct> export type AdminCreateProductType = z.infer<typeof CreateProduct>
export const AdminCreateProduct = z export const CreateProduct = z
.object({ .object({
title: z.string(), title: z.string(),
subtitle: z.string().nullish(), subtitle: z.string().nullish(),
@@ -220,8 +266,8 @@ export const AdminCreateProduct = z
collection_id: z.string().nullish(), collection_id: z.string().nullish(),
categories: z.array(IdAssociation).optional(), categories: z.array(IdAssociation).optional(),
tags: z.array(IdAssociation).optional(), tags: z.array(IdAssociation).optional(),
options: z.array(AdminCreateProductOption).optional(), options: z.array(CreateProductOption).optional(),
variants: z.array(AdminCreateProductVariant).optional(), variants: z.array(CreateProductVariant).optional(),
sales_channels: z.array(z.object({ id: z.string() })).optional(), sales_channels: z.array(z.object({ id: z.string() })).optional(),
weight: z.number().nullish(), weight: z.number().nullish(),
length: z.number().nullish(), length: z.number().nullish(),
@@ -235,14 +281,28 @@ export const AdminCreateProduct = z
}) })
.strict() .strict()
export type AdminUpdateProductType = z.infer<typeof AdminUpdateProduct> export const AdminCreateProduct = (
export const AdminUpdateProduct = z 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({ .object({
title: z.string().optional(), title: z.string().optional(),
discountable: z.boolean().optional(), discountable: z.boolean().optional(),
is_giftcard: z.boolean().optional(), is_giftcard: z.boolean().optional(),
options: z.array(AdminUpdateProductOption).optional(), options: z.array(UpdateProductOption).optional(),
variants: z.array(AdminUpdateProductVariant).optional(), variants: z.array(UpdateProductVariant).optional(),
status: statusEnum.optional(), status: statusEnum.optional(),
subtitle: z.string().nullish(), subtitle: z.string().nullish(),
description: z.string().nullish(), description: z.string().nullish(),
@@ -266,10 +326,24 @@ export const AdminUpdateProduct = z
}) })
.strict() .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< export type AdminBatchUpdateProductType = z.infer<
typeof AdminBatchUpdateProduct typeof AdminBatchUpdateProduct
> >
export const AdminBatchUpdateProduct = AdminUpdateProduct.extend({ export const AdminBatchUpdateProduct = UpdateProduct.extend({
id: z.string(), id: z.string(),
}) })
@@ -9,7 +9,7 @@ describe("validateAndTransformBody", () => {
jest.clearAllMocks() jest.clearAllMocks()
}) })
it("should merge a custom validators schema", async () => { it("should pass additionalDataValidator to validator factory", async () => {
let mockRequest = { let mockRequest = {
query: {}, query: {},
body: {}, body: {},
@@ -18,42 +18,16 @@ describe("validateAndTransformBody", () => {
const mockResponse = {} as MedusaResponse const mockResponse = {} as MedusaResponse
const nextFunction = jest.fn() const nextFunction = jest.fn()
mockRequest.extendedValidators = { mockRequest.additionalDataValidator = zod.object({
body: zod.object({ brand_id: zod.number(),
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>) => { const validatorFactory = (schema?: Zod.ZodObject<any, any>) => {
return schema ? createLinkBody().merge(schema) : createLinkBody() return schema ? createLinkBody().merge(schema) : createLinkBody()
} }
let middleware = validateAndTransformBody(validatorFactory) let middleware = validateAndTransformBody(validatorFactory)
await middleware(mockRequest, mockResponse, nextFunction) await middleware(mockRequest, mockResponse, nextFunction)
expect(nextFunction).toHaveBeenCalledWith( expect(nextFunction).toHaveBeenCalledWith(
new MedusaError( new MedusaError(
@@ -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`
)
)
})
}) })
@@ -17,12 +17,8 @@ export function validateAndTransformBody(
return async (req: MedusaRequest, _: MedusaResponse, next: NextFunction) => { return async (req: MedusaRequest, _: MedusaResponse, next: NextFunction) => {
try { try {
let schema: z.ZodObject<any, any> | z.ZodEffects<any, any> let schema: z.ZodObject<any, any> | z.ZodEffects<any, any>
const { body: bodyValidatorToMerge } = req.extendedValidators ?? {}
if (typeof zodSchema === "function") { if (typeof zodSchema === "function") {
schema = zodSchema(bodyValidatorToMerge) schema = zodSchema(req.additionalDataValidator)
} else if (bodyValidatorToMerge) {
schema = zodSchema.merge(bodyValidatorToMerge)
} else { } else {
schema = zodSchema schema = zodSchema
} }
@@ -58,11 +58,7 @@ const getFilterableFields = <T extends RequestQueryFields>(obj: T): T => {
} }
export function validateAndTransformQuery<TEntity extends BaseEntity>( export function validateAndTransformQuery<TEntity extends BaseEntity>(
zodSchema: zodSchema: z.ZodObject<any, any> | z.ZodEffects<any, any>,
| 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,
@@ -75,18 +71,7 @@ export function validateAndTransformQuery<TEntity extends BaseEntity>(
delete req.allowed delete req.allowed
const query = normalizeQuery(req) const query = normalizeQuery(req)
let schema: z.ZodObject<any, any> | z.ZodEffects<any, any> const validated = await zodValidator(zodSchema, query)
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 })
+4 -6
View File
@@ -63,14 +63,12 @@ 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 * Custom validator to validate the `additional_data` property in
* merged with the original validator of the route. * requests that allows for additional_data
*/ */
extendedValidators?: { additionalDataValidator?: ZodObject<any, any>
body?: ZodObject<any, any>
queryParams?: ZodObject<any, any>
}
} }
export interface AuthContext { export interface AuthContext {
@@ -21,22 +21,20 @@ describe("defineMiddlewares", function () {
}) })
}) })
test("should wrap body extendedValidator to middleware", () => { test("should wrap additionalDataValidator to middleware", () => {
const req = { const req = {
body: {}, body: {},
} as MedusaRequest } as MedusaRequest
const res = {} as MedusaResponse const res = {} as MedusaResponse
const nextFn = jest.fn() const nextFn = jest.fn()
const schema = zod.object({ const schema = {
brand_id: zod.string(), brand_id: zod.string(),
}) }
const config = defineMiddlewares([ const config = defineMiddlewares([
{ {
matcher: "/admin/products", matcher: "/admin/products",
extendedValidators: { additionalDataValidator: schema,
body: schema,
},
}, },
]) ])
@@ -50,42 +48,10 @@ describe("defineMiddlewares", function () {
}) })
config.routes?.[0].middlewares?.[0](req, res, nextFn) config.routes?.[0].middlewares?.[0](req, res, nextFn)
expect(req.extendedValidators).toMatchObject({ expect(req.additionalDataValidator!.parse({ brand_id: "1" })).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([
{ {
matcher: "/admin/products", brand_id: "1",
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,
})
}) })
}) })