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:
1 parent
c3efac5a0d
commit
fd83e75e4b
25 files changed
+781
-44
No files matched your search
@@ -2828,6 +2828,163 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("batch methods", () => {
|
||||||
|
it("successfully creates, updates, and deletes products", async () => {
|
||||||
|
await breaking(
|
||||||
|
() => {},
|
||||||
|
async () => {
|
||||||
|
const createPayload = getProductFixture({
|
||||||
|
title: "Test batch create",
|
||||||
|
handle: "test-batch-create",
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatePayload = {
|
||||||
|
id: publishedProduct.id,
|
||||||
|
title: "Test batch update",
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
"/admin/products/op/batch",
|
||||||
|
{
|
||||||
|
create: [createPayload],
|
||||||
|
update: [updatePayload],
|
||||||
|
delete: [baseProduct.id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.created).toHaveLength(1)
|
||||||
|
expect(response.data.updated).toHaveLength(1)
|
||||||
|
expect(response.data.deleted.ids).toHaveLength(1)
|
||||||
|
|
||||||
|
expect(response.data.created).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Test batch create",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(response.data.updated).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Test batch update",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(response.data.deleted).toEqual(
|
||||||
|
expect.objectContaining({ ids: [baseProduct.id] })
|
||||||
|
)
|
||||||
|
|
||||||
|
const dbData = (await api.get("/admin/products", adminHeaders))
|
||||||
|
.data.products
|
||||||
|
|
||||||
|
expect(dbData).toHaveLength(3)
|
||||||
|
expect(dbData).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Test batch create",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Test batch update",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully creates, updates, and deletes product variants", async () => {
|
||||||
|
await breaking(
|
||||||
|
() => {},
|
||||||
|
async () => {
|
||||||
|
const productWithMultipleVariants = getProductFixture({
|
||||||
|
title: "Test batch variants",
|
||||||
|
handle: "test-batch-variants",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Variant 1",
|
||||||
|
inventory_quantity: 5,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Variant 2",
|
||||||
|
inventory_quantity: 20,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 200,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const createdProduct = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
productWithMultipleVariants,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
const createPayload = {
|
||||||
|
title: "Test batch create variant",
|
||||||
|
inventory_quantity: 10,
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "dkk",
|
||||||
|
amount: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePayload = {
|
||||||
|
id: createdProduct.variants[0].id,
|
||||||
|
title: "Test batch update variant",
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/products/${createdProduct.id}/variants/op/batch`,
|
||||||
|
{
|
||||||
|
create: [createPayload],
|
||||||
|
update: [updatePayload],
|
||||||
|
delete: [createdProduct.variants[1].id],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const dbData = (
|
||||||
|
await api.get(
|
||||||
|
`/admin/products/${createdProduct.id}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product.variants
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(dbData).toHaveLength(2)
|
||||||
|
expect(dbData).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Test batch create variant",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Test batch update variant",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// TODO: Discuss how this should be handled
|
// TODO: Discuss how this should be handled
|
||||||
describe.skip("GET /admin/products/tag-usage", () => {
|
describe.skip("GET /admin/products/tag-usage", () => {
|
||||||
it("successfully gets the tags usage", async () => {
|
it("successfully gets the tags usage", async () => {
|
||||||
|
|||||||
@@ -1,27 +1,48 @@
|
|||||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
import { IPricingModuleService, PricingTypes } from "@medusajs/types"
|
import { IPricingModuleService, PricingTypes } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
convertItemResponseToUpdateRequest,
|
MedusaError,
|
||||||
getSelectsAndRelationsFromObjectArray,
|
getSelectsAndRelationsFromObjectArray,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
type UpdatePriceSetsStepInput = {
|
type UpdatePriceSetsStepInput =
|
||||||
|
| {
|
||||||
selector?: PricingTypes.FilterablePriceSetProps
|
selector?: PricingTypes.FilterablePriceSetProps
|
||||||
update?: PricingTypes.UpdatePriceSetDTO
|
update?: PricingTypes.UpdatePriceSetDTO
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
price_sets: PricingTypes.UpsertPriceSetDTO[]
|
||||||
|
}
|
||||||
|
|
||||||
export const updatePriceSetsStepId = "update-price-sets"
|
export const updatePriceSetsStepId = "update-price-sets"
|
||||||
export const updatePriceSetsStep = createStep(
|
export const updatePriceSetsStep = createStep(
|
||||||
updatePriceSetsStepId,
|
updatePriceSetsStepId,
|
||||||
async (data: UpdatePriceSetsStepInput, { container }) => {
|
async (data: UpdatePriceSetsStepInput, { container }) => {
|
||||||
if (!data.selector || !data.update) {
|
|
||||||
return new StepResponse([], null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pricingModule = container.resolve<IPricingModuleService>(
|
const pricingModule = container.resolve<IPricingModuleService>(
|
||||||
ModuleRegistrationName.PRICING
|
ModuleRegistrationName.PRICING
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if ("price_sets" in data) {
|
||||||
|
if (data.price_sets.some((p) => !p.id)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
"Price set id is required when doing a batch update"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevData = await pricingModule.list({
|
||||||
|
id: data.price_sets.map((p) => p.id) as string[],
|
||||||
|
})
|
||||||
|
|
||||||
|
const priceSets = await pricingModule.upsert(data.price_sets)
|
||||||
|
return new StepResponse(priceSets, prevData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.selector || !data.update) {
|
||||||
|
return new StepResponse([], null)
|
||||||
|
}
|
||||||
|
|
||||||
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
|
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
|
||||||
data.update,
|
data.update,
|
||||||
])
|
])
|
||||||
@@ -36,27 +57,17 @@ export const updatePriceSetsStep = createStep(
|
|||||||
data.update
|
data.update
|
||||||
)
|
)
|
||||||
|
|
||||||
return new StepResponse(updatedPriceSets, {
|
return new StepResponse(updatedPriceSets, dataBeforeUpdate)
|
||||||
dataBeforeUpdate,
|
|
||||||
selects,
|
|
||||||
relations,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
async (revertInput, { container }) => {
|
async (revertInput, { container }) => {
|
||||||
if (!revertInput || !revertInput.dataBeforeUpdate?.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { dataBeforeUpdate, selects, relations } = revertInput
|
|
||||||
|
|
||||||
const pricingModule = container.resolve<IPricingModuleService>(
|
const pricingModule = container.resolve<IPricingModuleService>(
|
||||||
ModuleRegistrationName.PRICING
|
ModuleRegistrationName.PRICING
|
||||||
)
|
)
|
||||||
|
|
||||||
await pricingModule.upsert(
|
if (!revertInput) {
|
||||||
dataBeforeUpdate.map((data) =>
|
return
|
||||||
convertItemResponseToUpdateRequest(data, selects, relations)
|
}
|
||||||
)
|
|
||||||
)
|
await pricingModule.upsert(revertInput as PricingTypes.UpsertPriceSetDTO[])
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
import { deleteProductVariantsWorkflow } from "../workflows/delete-product-variants"
|
||||||
|
import { createProductVariantsWorkflow } from "../workflows/create-product-variants"
|
||||||
|
import { updateProductVariantsWorkflow } from "../workflows/update-product-variants"
|
||||||
|
import { PricingTypes, ProductTypes } from "@medusajs/types"
|
||||||
|
|
||||||
|
type BatchProductVariantsInput = {
|
||||||
|
create: (ProductTypes.CreateProductVariantDTO & {
|
||||||
|
prices?: PricingTypes.CreateMoneyAmountDTO[]
|
||||||
|
})[]
|
||||||
|
update: (ProductTypes.UpsertProductVariantDTO & {
|
||||||
|
prices?: PricingTypes.CreateMoneyAmountDTO[]
|
||||||
|
})[]
|
||||||
|
delete: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const batchProductVariantsStepId = "batch-product-variants"
|
||||||
|
export const batchProductVariantsStep = createStep(
|
||||||
|
batchProductVariantsStepId,
|
||||||
|
async (data: BatchProductVariantsInput, { container }) => {
|
||||||
|
const { transaction: createTransaction, result: created } =
|
||||||
|
await createProductVariantsWorkflow(container).run({
|
||||||
|
input: { product_variants: data.create },
|
||||||
|
})
|
||||||
|
const { transaction: updateTransaction, result: updated } =
|
||||||
|
await updateProductVariantsWorkflow(container).run({
|
||||||
|
input: { product_variants: data.update },
|
||||||
|
})
|
||||||
|
const { transaction: deleteTransaction } =
|
||||||
|
await deleteProductVariantsWorkflow(container).run({
|
||||||
|
input: { ids: data.delete },
|
||||||
|
})
|
||||||
|
|
||||||
|
return new StepResponse(
|
||||||
|
{
|
||||||
|
created,
|
||||||
|
updated,
|
||||||
|
deleted: {
|
||||||
|
ids: data.delete,
|
||||||
|
object: "product_variant",
|
||||||
|
deleted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ createTransaction, updateTransaction, deleteTransaction }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
async (flow, { container }) => {
|
||||||
|
if (!flow) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flow.createTransaction) {
|
||||||
|
await createProductVariantsWorkflow(container).cancel({
|
||||||
|
transaction: flow.createTransaction,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flow.updateTransaction) {
|
||||||
|
await updateProductVariantsWorkflow(container).cancel({
|
||||||
|
transaction: flow.updateTransaction,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flow.deleteTransaction) {
|
||||||
|
await deleteProductVariantsWorkflow(container).cancel({
|
||||||
|
transaction: flow.deleteTransaction,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
import { createProductsWorkflow } from "../workflows/create-products"
|
||||||
|
import { updateProductsWorkflow } from "../workflows/update-products"
|
||||||
|
import { deleteProductsWorkflow } from "../workflows/delete-products"
|
||||||
|
import { PricingTypes, ProductTypes } from "@medusajs/types"
|
||||||
|
|
||||||
|
type WorkflowInput = {
|
||||||
|
create: (Omit<ProductTypes.CreateProductDTO, "variants"> & {
|
||||||
|
sales_channels?: { id: string }[]
|
||||||
|
variants?: (ProductTypes.CreateProductVariantDTO & {
|
||||||
|
prices?: PricingTypes.CreateMoneyAmountDTO[]
|
||||||
|
})[]
|
||||||
|
})[]
|
||||||
|
update: (ProductTypes.UpsertProductDTO & {
|
||||||
|
sales_channels?: { id: string }[]
|
||||||
|
})[]
|
||||||
|
delete: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const batchProductsStepId = "batch-products"
|
||||||
|
export const batchProductsStep = createStep(
|
||||||
|
batchProductsStepId,
|
||||||
|
async (data: WorkflowInput, { container }) => {
|
||||||
|
const { transaction: createTransaction, result: created } =
|
||||||
|
await createProductsWorkflow(container).run({
|
||||||
|
input: { products: data.create },
|
||||||
|
})
|
||||||
|
const { transaction: updateTransaction, result: updated } =
|
||||||
|
await updateProductsWorkflow(container).run({
|
||||||
|
input: { products: data.update },
|
||||||
|
})
|
||||||
|
const { transaction: deleteTransaction } = await deleteProductsWorkflow(
|
||||||
|
container
|
||||||
|
).run({
|
||||||
|
input: { ids: data.delete },
|
||||||
|
})
|
||||||
|
|
||||||
|
return new StepResponse(
|
||||||
|
{
|
||||||
|
created,
|
||||||
|
updated,
|
||||||
|
deleted: {
|
||||||
|
ids: data.delete,
|
||||||
|
object: "product",
|
||||||
|
deleted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ createTransaction, updateTransaction, deleteTransaction }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
async (flow, { container }) => {
|
||||||
|
if (!flow) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flow.createTransaction) {
|
||||||
|
await createProductsWorkflow(container).cancel({
|
||||||
|
transaction: flow.createTransaction,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flow.updateTransaction) {
|
||||||
|
await updateProductsWorkflow(container).cancel({
|
||||||
|
transaction: flow.updateTransaction,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flow.deleteTransaction) {
|
||||||
|
await deleteProductsWorkflow(container).cancel({
|
||||||
|
transaction: flow.deleteTransaction,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -2,6 +2,7 @@ export * from "./create-products"
|
|||||||
export * from "./update-products"
|
export * from "./update-products"
|
||||||
export * from "./delete-products"
|
export * from "./delete-products"
|
||||||
export * from "./get-products"
|
export * from "./get-products"
|
||||||
|
export * from "./batch-products"
|
||||||
export * from "./create-variant-pricing-link"
|
export * from "./create-variant-pricing-link"
|
||||||
export * from "./create-product-options"
|
export * from "./create-product-options"
|
||||||
export * from "./update-product-options"
|
export * from "./update-product-options"
|
||||||
@@ -9,6 +10,7 @@ export * from "./delete-product-options"
|
|||||||
export * from "./create-product-variants"
|
export * from "./create-product-variants"
|
||||||
export * from "./update-product-variants"
|
export * from "./update-product-variants"
|
||||||
export * from "./delete-product-variants"
|
export * from "./delete-product-variants"
|
||||||
|
export * from "./batch-product-variants"
|
||||||
export * from "./create-collections"
|
export * from "./create-collections"
|
||||||
export * from "./update-collections"
|
export * from "./update-collections"
|
||||||
export * from "./delete-collections"
|
export * from "./delete-collections"
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
import { IProductModuleService, ProductTypes } from "@medusajs/types"
|
import { IProductModuleService, ProductTypes } from "@medusajs/types"
|
||||||
import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils"
|
import {
|
||||||
|
MedusaError,
|
||||||
|
getSelectsAndRelationsFromObjectArray,
|
||||||
|
} from "@medusajs/utils"
|
||||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
type UpdateProductVariantsStepInput = {
|
type UpdateProductVariantsStepInput =
|
||||||
|
| {
|
||||||
selector: ProductTypes.FilterableProductVariantProps
|
selector: ProductTypes.FilterableProductVariantProps
|
||||||
update: ProductTypes.UpdateProductVariantDTO
|
update: ProductTypes.UpdateProductVariantDTO
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
product_variants: ProductTypes.UpsertProductVariantDTO[]
|
||||||
|
}
|
||||||
|
|
||||||
export const updateProductVariantsStepId = "update-product-variants"
|
export const updateProductVariantsStepId = "update-product-variants"
|
||||||
export const updateProductVariantsStep = createStep(
|
export const updateProductVariantsStep = createStep(
|
||||||
@@ -16,6 +23,24 @@ export const updateProductVariantsStep = createStep(
|
|||||||
ModuleRegistrationName.PRODUCT
|
ModuleRegistrationName.PRODUCT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if ("product_variants" in data) {
|
||||||
|
if (data.product_variants.some((p) => !p.id)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
"Product variant ID is required when doing a batch update of product variants"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevData = await service.listVariants({
|
||||||
|
id: data.product_variants.map((p) => p.id) as string[],
|
||||||
|
})
|
||||||
|
|
||||||
|
const productVariants = await service.upsertVariants(
|
||||||
|
data.product_variants
|
||||||
|
)
|
||||||
|
return new StepResponse(productVariants, prevData)
|
||||||
|
}
|
||||||
|
|
||||||
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
|
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
|
||||||
data.update,
|
data.update,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
import { IProductModuleService, ProductTypes } from "@medusajs/types"
|
import { IProductModuleService, ProductTypes } from "@medusajs/types"
|
||||||
import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils"
|
import {
|
||||||
|
MedusaError,
|
||||||
|
getSelectsAndRelationsFromObjectArray,
|
||||||
|
} from "@medusajs/utils"
|
||||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
type UpdateProductsStepInput = {
|
type UpdateProductsStepInput =
|
||||||
|
| {
|
||||||
selector: ProductTypes.FilterableProductProps
|
selector: ProductTypes.FilterableProductProps
|
||||||
update: ProductTypes.UpdateProductDTO
|
update: ProductTypes.UpdateProductDTO
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
products: ProductTypes.UpsertProductDTO[]
|
||||||
|
}
|
||||||
|
|
||||||
export const updateProductsStepId = "update-products"
|
export const updateProductsStepId = "update-products"
|
||||||
export const updateProductsStep = createStep(
|
export const updateProductsStep = createStep(
|
||||||
@@ -16,6 +23,22 @@ export const updateProductsStep = createStep(
|
|||||||
ModuleRegistrationName.PRODUCT
|
ModuleRegistrationName.PRODUCT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if ("products" in data) {
|
||||||
|
if (data.products.some((p) => !p.id)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
"Product ID is required when doing a batch update of products"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevData = await service.list({
|
||||||
|
id: data.products.map((p) => p.id) as string[],
|
||||||
|
})
|
||||||
|
|
||||||
|
const products = await service.upsert(data.products)
|
||||||
|
return new StepResponse(products, prevData)
|
||||||
|
}
|
||||||
|
|
||||||
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
|
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
|
||||||
data.update,
|
data.update,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||||
|
import { batchProductVariantsStep } from "../steps/batch-product-variants"
|
||||||
|
import { PricingTypes, ProductTypes } from "@medusajs/types"
|
||||||
|
|
||||||
|
type BatchProductVariantsInput = {
|
||||||
|
create: (ProductTypes.CreateProductVariantDTO & {
|
||||||
|
prices?: PricingTypes.CreateMoneyAmountDTO[]
|
||||||
|
})[]
|
||||||
|
update: (ProductTypes.UpsertProductVariantDTO & {
|
||||||
|
prices?: PricingTypes.CreateMoneyAmountDTO[]
|
||||||
|
})[]
|
||||||
|
delete: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatchProductVariantsOutput = {
|
||||||
|
created: ProductTypes.ProductVariantDTO[]
|
||||||
|
updated: ProductTypes.ProductVariantDTO[]
|
||||||
|
deleted: {
|
||||||
|
ids: string[]
|
||||||
|
object: string
|
||||||
|
deleted: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const batchProductVariantsWorkflowId = "batch-product-variants"
|
||||||
|
export const batchProductVariantsWorkflow = createWorkflow(
|
||||||
|
batchProductVariantsWorkflowId,
|
||||||
|
(
|
||||||
|
input: WorkflowData<BatchProductVariantsInput>
|
||||||
|
): WorkflowData<BatchProductVariantsOutput> => {
|
||||||
|
return batchProductVariantsStep(input)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||||
|
import { batchProductsStep } from "../steps/batch-products"
|
||||||
|
import { PricingTypes, ProductTypes } from "@medusajs/types"
|
||||||
|
|
||||||
|
type WorkflowInput = {
|
||||||
|
create: (Omit<ProductTypes.CreateProductDTO, "variants"> & {
|
||||||
|
sales_channels?: { id: string }[]
|
||||||
|
variants?: (ProductTypes.CreateProductVariantDTO & {
|
||||||
|
prices?: PricingTypes.CreateMoneyAmountDTO[]
|
||||||
|
})[]
|
||||||
|
})[]
|
||||||
|
update: (ProductTypes.UpsertProductDTO & {
|
||||||
|
sales_channels?: { id: string }[]
|
||||||
|
})[]
|
||||||
|
delete: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatchProductsOutput = {
|
||||||
|
created: ProductTypes.ProductDTO[]
|
||||||
|
updated: ProductTypes.ProductDTO[]
|
||||||
|
deleted: {
|
||||||
|
ids: string[]
|
||||||
|
object: string
|
||||||
|
deleted: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const batchProductsWorkflowId = "batch-products"
|
||||||
|
export const batchProductsWorkflow = createWorkflow(
|
||||||
|
batchProductsWorkflowId,
|
||||||
|
(input: WorkflowData<WorkflowInput>): WorkflowData<BatchProductsOutput> => {
|
||||||
|
return batchProductsStep(input)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -4,7 +4,8 @@ import {
|
|||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
} from "@medusajs/workflows-sdk"
|
} from "@medusajs/workflows-sdk"
|
||||||
import { createProductsStep, createVariantPricingLinkStep } from "../steps"
|
import { createProductsStep } from "../steps/create-products"
|
||||||
|
import { createVariantPricingLinkStep } from "../steps/create-variant-pricing-link"
|
||||||
import { createPriceSetsStep } from "../../pricing"
|
import { createPriceSetsStep } from "../../pricing"
|
||||||
import { associateProductsWithSalesChannelsStep } from "../../sales-channel"
|
import { associateProductsWithSalesChannelsStep } from "../../sales-channel"
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import {
|
|||||||
transform,
|
transform,
|
||||||
} from "@medusajs/workflows-sdk"
|
} from "@medusajs/workflows-sdk"
|
||||||
import { Modules } from "@medusajs/modules-sdk"
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
import { deleteProductsStep, getProductsStep } from "../steps"
|
import { deleteProductsStep } from "../steps/delete-products"
|
||||||
|
import { getProductsStep } from "../steps/get-products"
|
||||||
import { removeRemoteLinkStep } from "../../common"
|
import { removeRemoteLinkStep } from "../../common"
|
||||||
|
|
||||||
type WorkflowInput = { ids: string[] }
|
type WorkflowInput = { ids: string[] }
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
export * from "./create-products"
|
export * from "./create-products"
|
||||||
export * from "./delete-products"
|
export * from "./delete-products"
|
||||||
export * from "./update-products"
|
export * from "./update-products"
|
||||||
|
export * from "./batch-products"
|
||||||
export * from "./create-product-options"
|
export * from "./create-product-options"
|
||||||
export * from "./delete-product-options"
|
export * from "./delete-product-options"
|
||||||
export * from "./update-product-options"
|
export * from "./update-product-options"
|
||||||
export * from "./create-product-variants"
|
export * from "./create-product-variants"
|
||||||
export * from "./delete-product-variants"
|
export * from "./delete-product-variants"
|
||||||
export * from "./update-product-variants"
|
export * from "./update-product-variants"
|
||||||
|
export * from "./batch-product-variants"
|
||||||
export * from "./create-collections"
|
export * from "./create-collections"
|
||||||
export * from "./delete-collections"
|
export * from "./delete-collections"
|
||||||
export * from "./update-collections"
|
export * from "./update-collections"
|
||||||
|
|||||||
@@ -8,12 +8,18 @@ import { updateProductVariantsStep } from "../steps"
|
|||||||
import { updatePriceSetsStep } from "../../pricing"
|
import { updatePriceSetsStep } from "../../pricing"
|
||||||
import { getVariantPricingLinkStep } from "../steps/get-variant-pricing-link"
|
import { getVariantPricingLinkStep } from "../steps/get-variant-pricing-link"
|
||||||
|
|
||||||
type UpdateProductVariantsStepInput = {
|
type UpdateProductVariantsStepInput =
|
||||||
|
| {
|
||||||
selector: ProductTypes.FilterableProductVariantProps
|
selector: ProductTypes.FilterableProductVariantProps
|
||||||
update: ProductTypes.UpdateProductVariantDTO & {
|
update: ProductTypes.UpdateProductVariantDTO & {
|
||||||
prices?: Partial<PricingTypes.CreateMoneyAmountDTO>[]
|
prices?: Partial<PricingTypes.CreateMoneyAmountDTO>[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
product_variants: (ProductTypes.UpsertProductVariantDTO & {
|
||||||
|
prices?: Partial<PricingTypes.CreateMoneyAmountDTO>[]
|
||||||
|
})[]
|
||||||
|
}
|
||||||
|
|
||||||
type WorkflowInput = UpdateProductVariantsStepInput
|
type WorkflowInput = UpdateProductVariantsStepInput
|
||||||
|
|
||||||
@@ -25,6 +31,17 @@ export const updateProductVariantsWorkflow = createWorkflow(
|
|||||||
): WorkflowData<ProductTypes.ProductVariantDTO[]> => {
|
): WorkflowData<ProductTypes.ProductVariantDTO[]> => {
|
||||||
// Passing prices to the product module will fail, we want to keep them for after the variant is updated.
|
// Passing prices to the product module will fail, we want to keep them for after the variant is updated.
|
||||||
const updateWithoutPrices = transform({ input }, (data) => {
|
const updateWithoutPrices = transform({ input }, (data) => {
|
||||||
|
if ("product_variants" in data.input) {
|
||||||
|
return {
|
||||||
|
product_variants: data.input.product_variants.map((variant) => {
|
||||||
|
return {
|
||||||
|
...variant,
|
||||||
|
prices: undefined,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selector: data.input.selector,
|
selector: data.input.selector,
|
||||||
update: {
|
update: {
|
||||||
@@ -38,6 +55,10 @@ export const updateProductVariantsWorkflow = createWorkflow(
|
|||||||
|
|
||||||
// We don't want to do any pricing updates if the prices didn't change
|
// We don't want to do any pricing updates if the prices didn't change
|
||||||
const variantIds = transform({ input, updatedVariants }, (data) => {
|
const variantIds = transform({ input, updatedVariants }, (data) => {
|
||||||
|
if ("product_variants" in data.input) {
|
||||||
|
return data.updatedVariants.map((v) => v.id)
|
||||||
|
}
|
||||||
|
|
||||||
if (!data.input.update.prices) {
|
if (!data.input.update.prices) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -56,6 +77,19 @@ export const updateProductVariantsWorkflow = createWorkflow(
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("product_variants" in data.input) {
|
||||||
|
return data.variantPriceSetLinks.map((link) => {
|
||||||
|
const variant = (data.input as any).product_variants.find(
|
||||||
|
(v) => v.id === link.variant_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: link.price_set_id,
|
||||||
|
prices: variant.prices,
|
||||||
|
} as PricingTypes.UpsertPriceSetDTO
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selector: {
|
selector: {
|
||||||
id: data.variantPriceSetLinks.map((link) => link.price_set_id),
|
id: data.variantPriceSetLinks.map((link) => link.price_set_id),
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { ProductTypes } from "@medusajs/types"
|
import { ProductTypes } from "@medusajs/types"
|
||||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||||
import { updateProductsStep } from "../steps"
|
import { updateProductsStep } from "../steps/update-products"
|
||||||
|
|
||||||
type UpdateProductsStepInput = {
|
type UpdateProductsStepInput =
|
||||||
|
| {
|
||||||
selector: ProductTypes.FilterableProductProps
|
selector: ProductTypes.FilterableProductProps
|
||||||
update: ProductTypes.UpdateProductDTO
|
update: ProductTypes.UpdateProductDTO
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
products: ProductTypes.UpsertProductDTO[]
|
||||||
|
}
|
||||||
|
|
||||||
type WorkflowInput = UpdateProductsStepInput
|
type WorkflowInput = UpdateProductsStepInput
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ export const POST = async (
|
|||||||
req.remoteQueryConfig.fields
|
req.remoteQueryConfig.fields
|
||||||
)
|
)
|
||||||
res.status(200).json({ product: remapProductResponse(product) })
|
res.status(200).json({ product: remapProductResponse(product) })
|
||||||
Response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DELETE = async (
|
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"
|
} from "../../../../../types/routing"
|
||||||
|
|
||||||
import { createProductVariantsWorkflow } from "@medusajs/core-flows"
|
import { createProductVariantsWorkflow } from "@medusajs/core-flows"
|
||||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
import {
|
||||||
|
remoteQueryObjectFromString,
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
} from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
refetchProduct,
|
refetchProduct,
|
||||||
remapKeysForVariant,
|
remapKeysForVariant,
|
||||||
@@ -17,7 +20,7 @@ export const GET = async (
|
|||||||
req: AuthenticatedMedusaRequest,
|
req: AuthenticatedMedusaRequest,
|
||||||
res: MedusaResponse
|
res: MedusaResponse
|
||||||
) => {
|
) => {
|
||||||
const remoteQuery = req.scope.resolve("remoteQuery")
|
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
const productId = req.params.id
|
const productId = req.params.id
|
||||||
|
|
||||||
const queryObject = remoteQueryObjectFromString({
|
const queryObject = remoteQueryObjectFromString({
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { MedusaContainer, ProductDTO, ProductVariantDTO } from "@medusajs/types"
|
import {
|
||||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
BatchMethodResponse,
|
||||||
|
MedusaContainer,
|
||||||
|
ProductDTO,
|
||||||
|
ProductVariantDTO,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||||
|
import { promiseAll, remoteQueryObjectFromString } from "@medusajs/utils"
|
||||||
|
|
||||||
const isPricing = (fieldName: string) =>
|
const isPricing = (fieldName: string) =>
|
||||||
fieldName.startsWith("variants.prices") ||
|
fieldName.startsWith("variants.prices") ||
|
||||||
@@ -67,7 +73,7 @@ export const refetchProduct = async (
|
|||||||
scope: MedusaContainer,
|
scope: MedusaContainer,
|
||||||
fields: string[]
|
fields: string[]
|
||||||
) => {
|
) => {
|
||||||
const remoteQuery = scope.resolve("remoteQuery")
|
const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
const queryObject = remoteQueryObjectFromString({
|
const queryObject = remoteQueryObjectFromString({
|
||||||
entryPoint: "product",
|
entryPoint: "product",
|
||||||
variables: {
|
variables: {
|
||||||
@@ -79,3 +85,85 @@ export const refetchProduct = async (
|
|||||||
const products = await remoteQuery(queryObject)
|
const products = await remoteQuery(queryObject)
|
||||||
return products[0]
|
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 { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
|
||||||
import { authenticate } from "../../../utils/authenticate-middleware"
|
import { authenticate } from "../../../utils/authenticate-middleware"
|
||||||
import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter"
|
import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter"
|
||||||
import { validateAndTransformBody } from "../../utils/validate-body"
|
import { validateAndTransformBody } from "../../utils/validate-body"
|
||||||
import { validateAndTransformQuery } from "../../utils/validate-query"
|
import { validateAndTransformQuery } from "../../utils/validate-query"
|
||||||
|
import { createBatchBody } from "../../utils/validators"
|
||||||
import * as QueryConfig from "./query-config"
|
import * as QueryConfig from "./query-config"
|
||||||
import { maybeApplyPriceListsFilter } from "./utils"
|
import { maybeApplyPriceListsFilter } from "./utils"
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +19,8 @@ import {
|
|||||||
AdminUpdateProductVariant,
|
AdminUpdateProductVariant,
|
||||||
AdminGetProductOptionsParams,
|
AdminGetProductOptionsParams,
|
||||||
AdminGetProductOptionParams,
|
AdminGetProductOptionParams,
|
||||||
|
AdminBatchUpdateProduct,
|
||||||
|
AdminBatchUpdateProductVariant,
|
||||||
} from "./validators"
|
} from "./validators"
|
||||||
|
|
||||||
export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
|
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"],
|
method: ["POST"],
|
||||||
matcher: "/admin/products/:id",
|
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
|
// Note: New endpoint in v2
|
||||||
{
|
{
|
||||||
method: ["GET"],
|
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(),
|
manage_inventory: z.boolean().optional(),
|
||||||
}).strict()
|
}).strict()
|
||||||
|
|
||||||
|
export type AdminBatchUpdateProductVariantType = z.infer<
|
||||||
|
typeof AdminBatchUpdateProductVariant
|
||||||
|
>
|
||||||
|
export const AdminBatchUpdateProductVariant = AdminUpdateProductVariant.extend({
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
export type AdminCreateProductType = z.infer<typeof AdminCreateProduct>
|
export type AdminCreateProductType = z.infer<typeof AdminCreateProduct>
|
||||||
export const AdminCreateProduct = z
|
export const AdminCreateProduct = z
|
||||||
.object({
|
.object({
|
||||||
@@ -228,6 +235,13 @@ export const AdminUpdateProduct = AdminCreateProduct.omit({ is_giftcard: true })
|
|||||||
})
|
})
|
||||||
.strict()
|
.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
|
// TODO: Handle in create and update product once ready
|
||||||
// @IsOptional()
|
// @IsOptional()
|
||||||
// @Type(() => ProductProductCategoryReq)
|
// @Type(() => ProductProductCategoryReq)
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import { z } from "zod"
|
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 = () => {
|
export const createSelectParams = () => {
|
||||||
return z.object({
|
return z.object({
|
||||||
fields: z.string().optional(),
|
fields: z.string().optional(),
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export type BatchMethodRequest<TCreate extends any, TUpdate extends any> = {
|
||||||
|
create?: TCreate[]
|
||||||
|
update?: TUpdate[]
|
||||||
|
delete?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BatchMethodResponse<T extends any> = {
|
||||||
|
created: T[]
|
||||||
|
updated: T[]
|
||||||
|
deleted: {
|
||||||
|
ids: string[]
|
||||||
|
object: string
|
||||||
|
deleted: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "./common"
|
export * from "./common"
|
||||||
export * from "./rule"
|
export * from "./rule"
|
||||||
|
export * from "./batch"
|
||||||
export * from "./config-module"
|
export * from "./config-module"
|
||||||
export * from "./medusa-container"
|
export * from "./medusa-container"
|
||||||
@@ -19,6 +19,8 @@ type StepFunctionReturnConfig<TOutput> = {
|
|||||||
): WorkflowData<TOutput>
|
): WorkflowData<TOutput>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KeysOfUnion<T> = T extends T ? keyof T : never
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A step function to be used in a workflow.
|
* A step function to be used in a workflow.
|
||||||
*
|
*
|
||||||
@@ -28,7 +30,7 @@ type StepFunctionReturnConfig<TOutput> = {
|
|||||||
export type StepFunction<
|
export type StepFunction<
|
||||||
TInput,
|
TInput,
|
||||||
TOutput = unknown
|
TOutput = unknown
|
||||||
> = (keyof TInput extends never
|
> = (KeysOfUnion<TInput> extends []
|
||||||
? // Function that doesn't expect any input
|
? // Function that doesn't expect any input
|
||||||
{
|
{
|
||||||
(): WorkflowData<TOutput> & StepFunctionReturnConfig<TOutput>
|
(): WorkflowData<TOutput> & StepFunctionReturnConfig<TOutput>
|
||||||
|
|||||||
Reference in new issue
Block a user