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
2024-04-15 16:48:29 +02:00
committed by GitHub
parent c3efac5a0d
commit fd83e75e4b
25 changed files with 793 additions and 56 deletions
@@ -1,27 +1,48 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPricingModuleService, PricingTypes } from "@medusajs/types"
import {
convertItemResponseToUpdateRequest,
MedusaError,
getSelectsAndRelationsFromObjectArray,
} from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
type UpdatePriceSetsStepInput = {
selector?: PricingTypes.FilterablePriceSetProps
update?: PricingTypes.UpdatePriceSetDTO
}
type UpdatePriceSetsStepInput =
| {
selector?: PricingTypes.FilterablePriceSetProps
update?: PricingTypes.UpdatePriceSetDTO
}
| {
price_sets: PricingTypes.UpsertPriceSetDTO[]
}
export const updatePriceSetsStepId = "update-price-sets"
export const updatePriceSetsStep = createStep(
updatePriceSetsStepId,
async (data: UpdatePriceSetsStepInput, { container }) => {
if (!data.selector || !data.update) {
return new StepResponse([], null)
}
const pricingModule = container.resolve<IPricingModuleService>(
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([
data.update,
])
@@ -36,27 +57,17 @@ export const updatePriceSetsStep = createStep(
data.update
)
return new StepResponse(updatedPriceSets, {
dataBeforeUpdate,
selects,
relations,
})
return new StepResponse(updatedPriceSets, dataBeforeUpdate)
},
async (revertInput, { container }) => {
if (!revertInput || !revertInput.dataBeforeUpdate?.length) {
return
}
const { dataBeforeUpdate, selects, relations } = revertInput
const pricingModule = container.resolve<IPricingModuleService>(
ModuleRegistrationName.PRICING
)
await pricingModule.upsert(
dataBeforeUpdate.map((data) =>
convertItemResponseToUpdateRequest(data, selects, relations)
)
)
if (!revertInput) {
return
}
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 "./delete-products"
export * from "./get-products"
export * from "./batch-products"
export * from "./create-variant-pricing-link"
export * from "./create-product-options"
export * from "./update-product-options"
@@ -9,6 +10,7 @@ export * from "./delete-product-options"
export * from "./create-product-variants"
export * from "./update-product-variants"
export * from "./delete-product-variants"
export * from "./batch-product-variants"
export * from "./create-collections"
export * from "./update-collections"
export * from "./delete-collections"
@@ -1,12 +1,19 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IProductModuleService, ProductTypes } from "@medusajs/types"
import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils"
import {
MedusaError,
getSelectsAndRelationsFromObjectArray,
} from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
type UpdateProductVariantsStepInput = {
selector: ProductTypes.FilterableProductVariantProps
update: ProductTypes.UpdateProductVariantDTO
}
type UpdateProductVariantsStepInput =
| {
selector: ProductTypes.FilterableProductVariantProps
update: ProductTypes.UpdateProductVariantDTO
}
| {
product_variants: ProductTypes.UpsertProductVariantDTO[]
}
export const updateProductVariantsStepId = "update-product-variants"
export const updateProductVariantsStep = createStep(
@@ -16,6 +23,24 @@ export const updateProductVariantsStep = createStep(
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([
data.update,
])
@@ -1,12 +1,19 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IProductModuleService, ProductTypes } from "@medusajs/types"
import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils"
import {
MedusaError,
getSelectsAndRelationsFromObjectArray,
} from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
type UpdateProductsStepInput = {
selector: ProductTypes.FilterableProductProps
update: ProductTypes.UpdateProductDTO
}
type UpdateProductsStepInput =
| {
selector: ProductTypes.FilterableProductProps
update: ProductTypes.UpdateProductDTO
}
| {
products: ProductTypes.UpsertProductDTO[]
}
export const updateProductsStepId = "update-products"
export const updateProductsStep = createStep(
@@ -16,6 +23,22 @@ export const updateProductsStep = createStep(
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([
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,
transform,
} 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 { associateProductsWithSalesChannelsStep } from "../../sales-channel"
@@ -4,7 +4,8 @@ import {
transform,
} from "@medusajs/workflows-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"
type WorkflowInput = { ids: string[] }
@@ -1,12 +1,14 @@
export * from "./create-products"
export * from "./delete-products"
export * from "./update-products"
export * from "./batch-products"
export * from "./create-product-options"
export * from "./delete-product-options"
export * from "./update-product-options"
export * from "./create-product-variants"
export * from "./delete-product-variants"
export * from "./update-product-variants"
export * from "./batch-product-variants"
export * from "./create-collections"
export * from "./delete-collections"
export * from "./update-collections"
@@ -8,12 +8,18 @@ import { updateProductVariantsStep } from "../steps"
import { updatePriceSetsStep } from "../../pricing"
import { getVariantPricingLinkStep } from "../steps/get-variant-pricing-link"
type UpdateProductVariantsStepInput = {
selector: ProductTypes.FilterableProductVariantProps
update: ProductTypes.UpdateProductVariantDTO & {
prices?: Partial<PricingTypes.CreateMoneyAmountDTO>[]
}
}
type UpdateProductVariantsStepInput =
| {
selector: ProductTypes.FilterableProductVariantProps
update: ProductTypes.UpdateProductVariantDTO & {
prices?: Partial<PricingTypes.CreateMoneyAmountDTO>[]
}
}
| {
product_variants: (ProductTypes.UpsertProductVariantDTO & {
prices?: Partial<PricingTypes.CreateMoneyAmountDTO>[]
})[]
}
type WorkflowInput = UpdateProductVariantsStepInput
@@ -25,6 +31,17 @@ export const updateProductVariantsWorkflow = createWorkflow(
): WorkflowData<ProductTypes.ProductVariantDTO[]> => {
// Passing prices to the product module will fail, we want to keep them for after the variant is updated.
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 {
selector: data.input.selector,
update: {
@@ -38,6 +55,10 @@ export const updateProductVariantsWorkflow = createWorkflow(
// We don't want to do any pricing updates if the prices didn't change
const variantIds = transform({ input, updatedVariants }, (data) => {
if ("product_variants" in data.input) {
return data.updatedVariants.map((v) => v.id)
}
if (!data.input.update.prices) {
return []
}
@@ -56,6 +77,19 @@ export const updateProductVariantsWorkflow = createWorkflow(
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 {
selector: {
id: data.variantPriceSetLinks.map((link) => link.price_set_id),
@@ -1,11 +1,15 @@
import { ProductTypes } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { updateProductsStep } from "../steps"
import { updateProductsStep } from "../steps/update-products"
type UpdateProductsStepInput = {
selector: ProductTypes.FilterableProductProps
update: ProductTypes.UpdateProductDTO
}
type UpdateProductsStepInput =
| {
selector: ProductTypes.FilterableProductProps
update: ProductTypes.UpdateProductDTO
}
| {
products: ProductTypes.UpsertProductDTO[]
}
type WorkflowInput = UpdateProductsStepInput