feat: Add support for batch method for products and product variants (#7038)

* feat(products): Add batch methods for product and variants

* chore: Rename batch validator and minor changes based on PR review
This commit is contained in:
Stevche Radevski authored and GitHub committed 2024-04-15 16:48:29 +02:00
1 parent c3efac5a0d
commit fd83e75e4b
25 files changed
+793 -56

No files matched your search

@@ -64,7 +64,6 @@ export const POST = async (
req.remoteQueryConfig.fields
)
res.status(200).json({ product: remapProductResponse(product) })
Response
}
export const DELETE = async (
@@ -0,0 +1,57 @@
import { batchProductVariantsWorkflow } from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../../../types/routing"
import {
AdminBatchUpdateProductVariantType,
AdminCreateProductType,
} from "../../../../validators"
import { BatchMethodRequest } from "@medusajs/types"
import { refetchBatchVariants, remapVariantResponse } from "../../../../helpers"
export const POST = async (
req: AuthenticatedMedusaRequest<
BatchMethodRequest<
AdminCreateProductType,
AdminBatchUpdateProductVariantType
>
>,
res: MedusaResponse
) => {
const productId = req.params.id
const normalizedInput = {
create: req.validatedBody.create?.map((c) => ({
...c,
product_id: productId,
})),
update: req.validatedBody.update?.map((u) => ({
...u,
product_id: productId,
})),
delete: req.validatedBody.delete,
// TODO: Fix types
} as any
const { result, errors } = await batchProductVariantsWorkflow(req.scope).run({
input: normalizedInput,
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
const batchResults = await refetchBatchVariants(
result,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({
created: batchResults.created.map(remapVariantResponse),
updated: batchResults.updated.map(remapVariantResponse),
deleted: batchResults.deleted,
})
}
@@ -4,7 +4,10 @@ import {
} from "../../../../../types/routing"
import { createProductVariantsWorkflow } from "@medusajs/core-flows"
import { remoteQueryObjectFromString } from "@medusajs/utils"
import {
remoteQueryObjectFromString,
ContainerRegistrationKeys,
} from "@medusajs/utils"
import {
refetchProduct,
remapKeysForVariant,
@@ -17,7 +20,7 @@ export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve("remoteQuery")
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const productId = req.params.id
const queryObject = remoteQueryObjectFromString({
@@ -1,5 +1,11 @@
import { MedusaContainer, ProductDTO, ProductVariantDTO } from "@medusajs/types"
import { remoteQueryObjectFromString } from "@medusajs/utils"
import {
BatchMethodResponse,
MedusaContainer,
ProductDTO,
ProductVariantDTO,
} from "@medusajs/types"
import { ContainerRegistrationKeys } from "@medusajs/utils"
import { promiseAll, remoteQueryObjectFromString } from "@medusajs/utils"
const isPricing = (fieldName: string) =>
fieldName.startsWith("variants.prices") ||
@@ -67,7 +73,7 @@ export const refetchProduct = async (
scope: MedusaContainer,
fields: string[]
) => {
const remoteQuery = scope.resolve("remoteQuery")
const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "product",
variables: {
@@ -79,3 +85,85 @@ export const refetchProduct = async (
const products = await remoteQuery(queryObject)
return products[0]
}
export const refetchBatchProducts = async (
batchResult: BatchMethodResponse<ProductDTO>,
scope: MedusaContainer,
fields: string[]
) => {
const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
let created = Promise.resolve<ProductDTO[]>([])
let updated = Promise.resolve<ProductDTO[]>([])
if (batchResult.created.length) {
const createdQuery = remoteQueryObjectFromString({
entryPoint: "product",
variables: {
filters: { id: batchResult.created.map((p) => p.id) },
},
fields: remapKeysForProduct(fields ?? []),
})
created = remoteQuery(createdQuery)
}
if (batchResult.updated.length) {
const updatedQuery = remoteQueryObjectFromString({
entryPoint: "product",
variables: {
filters: { id: batchResult.updated.map((p) => p.id) },
},
fields: remapKeysForProduct(fields ?? []),
})
updated = remoteQuery(updatedQuery)
}
const [createdRes, updatedRes] = await promiseAll([created, updated])
return {
created: createdRes,
updated: updatedRes,
deleted: batchResult.deleted,
}
}
export const refetchBatchVariants = async (
batchResult: BatchMethodResponse<ProductVariantDTO>,
scope: MedusaContainer,
fields: string[]
) => {
const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
let created = Promise.resolve<ProductVariantDTO[]>([])
let updated = Promise.resolve<ProductVariantDTO[]>([])
if (batchResult.created.length) {
const createdQuery = remoteQueryObjectFromString({
entryPoint: "variant",
variables: {
filters: { id: batchResult.created.map((v) => v.id) },
},
fields: remapKeysForVariant(fields ?? []),
})
created = remoteQuery(createdQuery)
}
if (batchResult.updated.length) {
const updatedQuery = remoteQueryObjectFromString({
entryPoint: "variant",
variables: {
filters: { id: batchResult.updated.map((v) => v.id) },
},
fields: remapKeysForVariant(fields ?? []),
})
updated = remoteQuery(updatedQuery)
}
const [createdRes, updatedRes] = await promiseAll([created, updated])
return {
created: createdRes,
updated: updatedRes,
deleted: batchResult.deleted,
}
}
@@ -1,9 +1,9 @@
import { transformBody, transformQuery } from "../../../api/middlewares"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware"
import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter"
import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query"
import { createBatchBody } from "../../utils/validators"
import * as QueryConfig from "./query-config"
import { maybeApplyPriceListsFilter } from "./utils"
import {
@@ -19,6 +19,8 @@ import {
AdminUpdateProductVariant,
AdminGetProductOptionsParams,
AdminGetProductOptionParams,
AdminBatchUpdateProduct,
AdminBatchUpdateProductVariant,
} from "./validators"
export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
@@ -64,6 +66,19 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
{
method: ["POST"],
matcher: "/admin/products/op/batch",
middlewares: [
validateAndTransformBody(
createBatchBody(AdminCreateProduct, AdminBatchUpdateProduct)
),
validateAndTransformQuery(
AdminGetProductParams,
QueryConfig.retrieveProductQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/products/:id",
@@ -96,6 +111,22 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
{
method: ["POST"],
matcher: "/admin/products/:id/variants/op/batch",
middlewares: [
validateAndTransformBody(
createBatchBody(
AdminCreateProductVariant,
AdminBatchUpdateProductVariant
)
),
validateAndTransformQuery(
AdminGetProductVariantParams,
QueryConfig.retrieveVariantConfig
),
],
},
// Note: New endpoint in v2
{
method: ["GET"],
@@ -0,0 +1,43 @@
import { batchProductsWorkflow } from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../types/routing"
import {
AdminBatchUpdateProductType,
AdminCreateProductType,
} from "../../validators"
import { BatchMethodRequest } from "@medusajs/types"
import { refetchBatchProducts, remapProductResponse } from "../../helpers"
import { CreateProductDTO, UpsertProductDTO } from "@medusajs/types"
export const POST = async (
req: AuthenticatedMedusaRequest<
BatchMethodRequest<AdminCreateProductType, AdminBatchUpdateProductType>
>,
res: MedusaResponse
) => {
// TODO: Fix types
const input = req.validatedBody as any
const { result, errors } = await batchProductsWorkflow(req.scope).run({
input,
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
const batchResults = await refetchBatchProducts(
result,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({
created: batchResults.created.map(remapProductResponse),
updated: batchResults.updated.map(remapProductResponse),
deleted: batchResults.deleted,
})
}
@@ -187,6 +187,13 @@ export const AdminUpdateProductVariant = AdminCreateProductVariant.extend({
manage_inventory: z.boolean().optional(),
}).strict()
export type AdminBatchUpdateProductVariantType = z.infer<
typeof AdminBatchUpdateProductVariant
>
export const AdminBatchUpdateProductVariant = AdminUpdateProductVariant.extend({
id: z.string(),
})
export type AdminCreateProductType = z.infer<typeof AdminCreateProduct>
export const AdminCreateProduct = z
.object({
@@ -228,6 +235,13 @@ export const AdminUpdateProduct = AdminCreateProduct.omit({ is_giftcard: true })
})
.strict()
export type AdminBatchUpdateProductType = z.infer<
typeof AdminBatchUpdateProduct
>
export const AdminBatchUpdateProduct = AdminUpdateProduct.extend({
id: z.string(),
})
// TODO: Handle in create and update product once ready
// @IsOptional()
// @Type(() => ProductProductCategoryReq)
@@ -1,5 +1,16 @@
import { z } from "zod"
export const createBatchBody = (
createValidator: z.ZodType,
updateValidator: z.ZodType
) => {
return z.object({
create: z.array(createValidator).optional(),
update: z.array(updateValidator).optional(),
delete: z.array(z.string()).optional(),
})
}
export const createSelectParams = () => {
return z.object({
fields: z.string().optional(),