feat(workflows): update product workflow (#4982)

**What**
- added "update product" workflow

Co-authored-by: Riqwan Thamir <5105988+riqwan@users.noreply.github.com>
This commit is contained in:
Frane Polić
2023-10-19 14:02:40 +02:00
committed by GitHub
parent 3aba6269ed
commit aba9ded2a3
34 changed files with 1511 additions and 126 deletions
@@ -1,3 +1,7 @@
import { DistributedTransaction } from "@medusajs/orchestration"
import { FlagRouter, MedusaError } from "@medusajs/utils"
import { Workflows, updateProducts } from "@medusajs/workflows"
import { Type } from "class-transformer"
import {
IsArray,
IsBoolean,
@@ -11,7 +15,13 @@ import {
ValidateIf,
ValidateNested,
} from "class-validator"
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
import { EntityManager } from "typeorm"
import {
defaultAdminProductFields,
defaultAdminProductRelations,
defaultAdminProductRemoteQueryObject,
} from "."
import { ProductStatus, ProductVariant } from "../../../../models"
import {
PricingService,
@@ -35,15 +45,13 @@ import {
revertVariantTransaction,
} from "./transaction/create-product-variant"
import { DistributedTransaction } from "@medusajs/orchestration"
import { IInventoryService } from "@medusajs/types"
import { MedusaError } from "@medusajs/utils"
import { Type } from "class-transformer"
import { EntityManager } from "typeorm"
import { IInventoryService, WorkflowTypes } from "@medusajs/types"
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { ProductVariantRepository } from "../../../../repositories/product-variant"
import { Logger } from "../../../../types/global"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain"
import { validator } from "../../../../utils/validator"
/**
@@ -129,134 +137,192 @@ export default async (req, res) => {
req.scope.resolve("inventoryService")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
const productServiceTx = productService.withTransaction(transactionManager)
const { variants } = validated
delete validated.variants
const productModuleService = req.scope.resolve("productModuleService")
const product = await productServiceTx.update(id, validated)
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
const isWorkflowEnabled = featureFlagRouter.isFeatureEnabled({
workflows: Workflows.UpdateProducts,
})
if (!variants) {
return
if (isWorkflowEnabled && !productModuleService) {
logger.warn(
`Cannot run ${Workflows.UpdateProducts} workflow without '@medusajs/product' installed`
)
}
if (isWorkflowEnabled && !!productModuleService) {
const updateProductWorkflow = updateProducts(req.scope)
const input = {
products: [
{ id, ...validated },
] as WorkflowTypes.ProductWorkflow.UpdateProductInputDTO[],
}
const variantRepo = manager.withRepository(productVariantRepo)
const productVariants = await productVariantService
.withTransaction(transactionManager)
.list(
{ product_id: id },
{
select: variantRepo.metadata.columns.map(
(c) => c.propertyName
) as (keyof ProductVariant)[],
const { result } = await updateProductWorkflow.run({
input,
context: {
manager: manager,
},
})
} else {
await manager.transaction(async (transactionManager) => {
const productServiceTx =
productService.withTransaction(transactionManager)
const { variants } = validated
delete validated.variants
const product = await productServiceTx.update(id, validated)
if (!variants) {
return
}
const variantRepo = manager.withRepository(productVariantRepo)
const productVariants = await productVariantService
.withTransaction(transactionManager)
.list(
{ product_id: id },
{
select: variantRepo.metadata.columns.map(
(c) => c.propertyName
) as (keyof ProductVariant)[],
}
)
const productVariantMap = new Map(productVariants.map((v) => [v.id, v]))
const variantWithIdSet = new Set()
const variantIdsNotBelongingToProduct: string[] = []
const variantsToUpdate: {
variant: ProductVariant
updateData: UpdateProductVariantInput
}[] = []
const variantsToCreate: ProductVariantReq[] = []
// Preparing the data step
for (const [variantRank, variant] of variants.entries()) {
if (!variant.id) {
Object.assign(variant, {
variant_rank: variantRank,
options: variant.options || [],
prices: variant.prices || [],
})
variantsToCreate.push(variant)
continue
}
)
const productVariantMap = new Map(productVariants.map((v) => [v.id, v]))
const variantWithIdSet = new Set()
// Will be used to find the variants that should be removed during the next steps
variantWithIdSet.add(variant.id)
const variantIdsNotBelongingToProduct: string[] = []
const variantsToUpdate: {
variant: ProductVariant
updateData: UpdateProductVariantInput
}[] = []
const variantsToCreate: ProductVariantReq[] = []
if (!productVariantMap.has(variant.id)) {
variantIdsNotBelongingToProduct.push(variant.id)
continue
}
// Preparing the data step
for (const [variantRank, variant] of variants.entries()) {
if (!variant.id) {
const productVariant = productVariantMap.get(variant.id)!
Object.assign(variant, {
variant_rank: variantRank,
options: variant.options || [],
prices: variant.prices || [],
product_id: productVariant.product_id,
})
variantsToCreate.push(variant)
continue
variantsToUpdate.push({ variant: productVariant, updateData: variant })
}
// Will be used to find the variants that should be removed during the next steps
variantWithIdSet.add(variant.id)
if (!productVariantMap.has(variant.id)) {
variantIdsNotBelongingToProduct.push(variant.id)
continue
if (variantIdsNotBelongingToProduct.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Variants with id: ${variantIdsNotBelongingToProduct.join(
", "
)} are not associated with this product`
)
}
const productVariant = productVariantMap.get(variant.id)!
Object.assign(variant, {
variant_rank: variantRank,
product_id: productVariant.product_id,
})
variantsToUpdate.push({ variant: productVariant, updateData: variant })
}
const allVariantTransactions: DistributedTransaction[] = []
const transactionDependencies = {
manager: transactionManager,
inventoryService,
productVariantInventoryService,
productVariantService,
}
if (variantIdsNotBelongingToProduct.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Variants with id: ${variantIdsNotBelongingToProduct.join(
", "
)} are not associated with this product`
const productVariantServiceTx =
productVariantService.withTransaction(transactionManager)
// Delete the variant that does not exist anymore from the provided variants
const variantIdsToDelete = [...productVariantMap.keys()].filter(
(variantId) => !variantWithIdSet.has(variantId)
)
}
const allVariantTransactions: DistributedTransaction[] = []
const transactionDependencies = {
manager: transactionManager,
inventoryService,
productVariantInventoryService,
productVariantService,
}
const productVariantServiceTx =
productVariantService.withTransaction(transactionManager)
// Delete the variant that does not exist anymore from the provided variants
const variantIdsToDelete = [...productVariantMap.keys()].filter(
(variantId) => !variantWithIdSet.has(variantId)
)
if (variantIdsToDelete) {
await productVariantServiceTx.delete(variantIdsToDelete)
}
if (variantsToUpdate.length) {
await productVariantServiceTx.update(variantsToUpdate)
}
if (variantsToCreate.length) {
try {
const varTransaction = await createVariantsTransaction(
transactionDependencies,
product.id,
variantsToCreate as CreateProductVariantInput[]
)
allVariantTransactions.push(varTransaction)
} catch (e) {
await Promise.all(
allVariantTransactions.map(async (transaction) => {
await revertVariantTransaction(
transactionDependencies,
transaction
).catch(() => logger.warn("Transaction couldn't be reverted."))
})
)
throw e
if (variantIdsToDelete) {
await productVariantServiceTx.delete(variantIdsToDelete)
}
}
})
const rawProduct = await productService.retrieve(id, {
select: defaultAdminProductFields,
relations: defaultAdminProductRelations,
})
if (variantsToUpdate.length) {
await productVariantServiceTx.update(variantsToUpdate)
}
if (variantsToCreate.length) {
try {
const varTransaction = await createVariantsTransaction(
transactionDependencies,
product.id,
variantsToCreate as CreateProductVariantInput[]
)
allVariantTransactions.push(varTransaction)
} catch (e) {
await Promise.all(
allVariantTransactions.map(async (transaction) => {
await revertVariantTransaction(
transactionDependencies,
transaction
).catch(() => logger.warn("Transaction couldn't be reverted."))
})
)
throw e
}
}
})
}
let rawProduct
if (featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key)) {
rawProduct = await getProductWithIsolatedProductModule(req, id)
} else {
rawProduct = await productService.retrieve(id, {
select: defaultAdminProductFields,
relations: defaultAdminProductRelations,
})
}
const [product] = await pricingService.setProductPrices([rawProduct])
res.json({ product })
}
async function getProductWithIsolatedProductModule(req, id) {
// TODO: Add support for fields/expands
const remoteQuery = req.scope.resolve("remoteQuery")
const variables = { id }
const query = {
product: {
__args: variables,
...defaultAdminProductRemoteQueryObject,
},
}
const [product] = await remoteQuery(query)
product.profile_id = product.profile?.id
return product
}
class ProductVariantOptionReq {
@IsString()
value: string