feat(orchestration,core-flows,medusa,product,types,utils): product import/export uses workflows (#5811)

This commit is contained in:
Riqwan Thamir
2023-12-12 12:09:25 +00:00
committed by GitHub
parent 6f96ced40f
commit 07107f3565
30 changed files with 1601 additions and 357 deletions
@@ -23,7 +23,6 @@ export async function createProductVariantsPrepareData({
container,
data,
}: WorkflowArguments<WorkflowTypes.ProductWorkflow.CreateProductVariantsWorkflowInputDTO>): Promise<CreateProductVariantsPreparedData> {
const featureFlagRouter = container.resolve("featureFlagRouter")
const productVariants: ProductWorkflow.CreateProductVariantsInputDTO[] =
data.productVariants || []
@@ -18,7 +18,10 @@ export async function updateProductVariants({
const productModuleService: ProductTypes.IProductModuleService =
container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName)
for (const [product_id, variantsUpdateData = []] of productVariantsMap) {
for (const [
product_id,
variantsUpdateData = [],
] of productVariantsMap) {
updateVariantsData.push(
...(variantsUpdateData as unknown as UpdateProductVariantOnlyDTO[]).map(
(update) => ({ ...update, product_id })
@@ -95,7 +95,9 @@ export async function upsertVariantPrices({
min_quantity: price.min_quantity,
max_quantity: price.max_quantity,
amount: price.amount,
currency_code: region_currency_code ?? price.currency_code,
currency_code: (
region_currency_code ?? price.currency_code
).toLowerCase(),
}
moneyAmountsToUpdate.push(priceToUpdate)
@@ -104,7 +106,9 @@ export async function upsertVariantPrices({
min_quantity: price.min_quantity,
max_quantity: price.max_quantity,
amount: price.amount,
currency_code: region_currency_code ?? price.currency_code,
currency_code: (
region_currency_code ?? price.currency_code
).toLowerCase(),
rules: region_rules ?? {},
}
@@ -19,7 +19,7 @@ import { pickBy } from "lodash"
import { ProductStatus } from "../../../../models"
import PriceListService from "../../../../services/price-list"
import { FilterableProductProps } from "../../../../types/product"
import { listAndCountProductWithIsolatedProductModule } from "../products/list-products"
import { listProducts } from "../../../../utils"
/**
* @oas [get] /admin/price-lists/{id}/products
@@ -196,8 +196,8 @@ export default async (req: Request, res) => {
}
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
;[products, count] = await listAndCountProductWithIsolatedProductModule(
req,
;[products, count] = await listProducts(
req.scope,
filterableFields,
req.listConfig
)
@@ -1,5 +1,5 @@
import { Workflows, createProducts } from "@medusajs/core-flows"
import { IInventoryService, WorkflowTypes } from "@medusajs/types"
import { createProducts, Workflows } from "@medusajs/core-flows"
import {
IsArray,
IsBoolean,
@@ -45,7 +45,7 @@ import { EntityManager } from "typeorm"
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { ProductStatus } from "../../../../models"
import { Logger } from "../../../../types/global"
import { validator } from "../../../../utils"
import { retrieveProduct, validator } from "../../../../utils"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
/**
@@ -257,7 +257,11 @@ export default async (req, res) => {
let rawProduct
if (isMedusaV2Enabled) {
rawProduct = await getProductWithIsolatedProductModule(req, product.id)
rawProduct = await retrieveProduct(
req.scope,
product.id,
defaultAdminProductRemoteQueryObject
)
} else {
rawProduct = await productService.retrieve(product.id, {
select: defaultAdminProductFields,
@@ -272,26 +276,6 @@ export default async (req, res) => {
res.json({ product: pricedProduct })
}
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
@@ -1,6 +1,7 @@
import { CreateProductVariants } from "@medusajs/core-flows"
import { IInventoryService, WorkflowTypes } from "@medusajs/types"
import { FlagRouter, MedusaV2Flag } from "@medusajs/utils"
import { CreateProductVariants } from "@medusajs/core-flows"
import { Type } from "class-transformer"
import {
IsArray,
IsBoolean,
@@ -10,7 +11,12 @@ import {
IsString,
ValidateNested,
} from "class-validator"
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
import { EntityManager } from "typeorm"
import {
defaultAdminProductFields,
defaultAdminProductRelations,
defaultAdminProductRemoteQueryObject,
} from "."
import {
PricingService,
ProductService,
@@ -21,10 +27,7 @@ import {
CreateProductVariantInput,
ProductVariantPricesCreateReq,
} from "../../../../types/product-variant"
import { Type } from "class-transformer"
import { EntityManager } from "typeorm"
import { validator } from "../../../../utils/validator"
import { getProductWithIsolatedProductModule } from "./get-product"
import { retrieveProduct, validator } from "../../../../utils"
import { createVariantsTransaction } from "./transaction/create-product-variant"
/**
@@ -157,10 +160,10 @@ export default async (req, res) => {
},
})
rawProduct = await getProductWithIsolatedProductModule(
req,
rawProduct = await retrieveProduct(
req.scope,
id,
req.retrieveConfig
defaultAdminProductRemoteQueryObject
)
} else {
await manager.transaction(async (transactionManager) => {
@@ -5,8 +5,9 @@ import {
SalesChannelService,
} from "../../../../services"
import { MedusaError, MedusaV2Flag, promiseAll } from "@medusajs/utils"
import { MedusaV2Flag, promiseAll } from "@medusajs/utils"
import { FindParams } from "../../../../types/common"
import { retrieveProduct } from "../../../../utils"
import { defaultAdminProductRemoteQueryObject } from "./index"
/**
@@ -76,10 +77,10 @@ export default async (req, res) => {
let rawProduct
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
rawProduct = await getProductWithIsolatedProductModule(
req,
rawProduct = await retrieveProduct(
req.scope,
id,
req.retrieveConfig
defaultAdminProductRemoteQueryObject
)
} else {
rawProduct = await productService.retrieve(id, req.retrieveConfig)
@@ -118,35 +119,4 @@ export default async (req, res) => {
res.json({ product })
}
export async function getProductWithIsolatedProductModule(
req,
id,
retrieveConfig
) {
// 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)
if (!product) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Product with id: ${id} not found`
)
}
product.profile_id = product.profile?.id
return product
}
export class AdminGetProductParams extends FindParams {}
@@ -1,23 +1,19 @@
import { IsNumber, IsOptional, IsString } from "class-validator"
import {
PriceListService,
PricingService,
ProductService,
ProductVariantInventoryService,
SalesChannelService,
} from "../../../../services"
import {
IInventoryService,
IPricingModuleService,
IProductModuleService,
} from "@medusajs/types"
import { MedusaV2Flag, promiseAll } from "@medusajs/utils"
import { listProducts } from "../../../../utils"
import { IInventoryService } from "@medusajs/types"
import { MedusaV2Flag } from "@medusajs/utils"
import { Type } from "class-transformer"
import { Product } from "../../../../models"
import { PricedProduct } from "../../../../types/pricing"
import { FilterableProductProps } from "../../../../types/product"
import { defaultAdminProductRemoteQueryObject } from "./index"
/**
* @oas [get] /admin/products
@@ -252,12 +248,11 @@ export default async (req, res) => {
let count
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
const [products, count_] =
await listAndCountProductWithIsolatedProductModule(
req,
req.filterableFields,
req.listConfig
)
const [products, count_] = await listProducts(
req.scope,
req.filterableFields,
req.listConfig
)
rawProducts = products
count = count_
@@ -305,171 +300,6 @@ export default async (req, res) => {
})
}
async function getVariantsFromPriceList(req, priceListId) {
const remoteQuery = req.scope.resolve("remoteQuery")
const pricingModuleService: IPricingModuleService = req.scope.resolve(
"pricingModuleService"
)
const productModuleService: IProductModuleService = req.scope.resolve(
"productModuleService"
)
const [priceList] = await pricingModuleService.listPriceLists(
{ id: [priceListId] },
{
relations: [
"price_set_money_amounts",
"price_set_money_amounts.price_set",
],
select: ["price_set_money_amounts.price_set.id"],
}
)
const priceSetIds = priceList.price_set_money_amounts?.map(
(psma) => psma.price_set?.id
)
const query = {
product_variant_price_set: {
__args: {
price_set_id: priceSetIds,
},
fields: ["variant_id", "price_set_id"],
},
}
const variantPriceSets = await remoteQuery(query)
const variantIds = variantPriceSets.map((vps) => vps.variant_id)
return await productModuleService.listVariants(
{
id: variantIds,
},
{
select: ["product_id"],
}
)
}
export async function listAndCountProductWithIsolatedProductModule(
req,
filterableFields,
listConfig
) {
// TODO: Add support for fields/expands
const remoteQuery = req.scope.resolve("remoteQuery")
const featureFlagRouter = req.scope.resolve("featureFlagRouter")
const productIdsFilter: Set<string> = new Set()
const variantIdsFilter: Set<string> = new Set()
const promises: Promise<void>[] = []
// This is not the best way of handling cross filtering but for now I would say it is fine
const salesChannelIdFilter = filterableFields.sales_channel_id
delete filterableFields.sales_channel_id
if (salesChannelIdFilter) {
const salesChannelService = req.scope.resolve(
"salesChannelService"
) as SalesChannelService
promises.push(
salesChannelService
.listProductIdsBySalesChannelIds(salesChannelIdFilter)
.then((productIdsInSalesChannel) => {
let filteredProductIds =
productIdsInSalesChannel[salesChannelIdFilter]
if (filterableFields.id) {
filterableFields.id = Array.isArray(filterableFields.id)
? filterableFields.id
: [filterableFields.id]
const salesChannelProductIdsSet = new Set(filteredProductIds)
filteredProductIds = filterableFields.id.filter((productId) =>
salesChannelProductIdsSet.has(productId)
)
}
filteredProductIds.map((id) => productIdsFilter.add(id))
})
)
}
const priceListId = filterableFields.price_list_id
delete filterableFields.price_list_id
if (priceListId) {
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
const variants = await getVariantsFromPriceList(req, priceListId)
variants.forEach((pv) => variantIdsFilter.add(pv.id))
} else {
// TODO: it is working but validate the behaviour.
// e.g pricing context properly set.
// At the moment filtering by price list but not having any customer id or
// include discount forces the query to filter with price list id is null
const priceListService = req.scope.resolve(
"priceListService"
) as PriceListService
promises.push(
priceListService
.listPriceListsVariantIdsMap(priceListId)
.then((priceListVariantIdsMap) => {
priceListVariantIdsMap[priceListId].map((variantId) =>
variantIdsFilter.add(variantId)
)
})
)
}
}
const discountConditionId = filterableFields.discount_condition_id
delete filterableFields.discount_condition_id
if (discountConditionId) {
// TODO implement later
}
await promiseAll(promises)
if (productIdsFilter.size > 0) {
filterableFields.id = Array.from(productIdsFilter)
}
if (variantIdsFilter.size > 0) {
filterableFields.variants = { id: Array.from(variantIdsFilter) }
}
const variables = {
filters: filterableFields,
order: listConfig.order,
skip: listConfig.skip,
take: listConfig.take,
}
const query = {
product: {
__args: variables,
...defaultAdminProductRemoteQueryObject,
},
}
const {
rows: products,
metadata: { count },
} = await remoteQuery(query)
products.forEach((product) => {
product.profile_id = product.profile?.id
})
return [products, count]
}
/**
* Parameters used to filter and configure the pagination of the retrieved products.
*/
@@ -1,3 +1,4 @@
import { Workflows, updateProducts } from "@medusajs/core-flows"
import { DistributedTransaction } from "@medusajs/orchestration"
import {
FlagRouter,
@@ -5,7 +6,6 @@ import {
MedusaV2Flag,
promiseAll,
} from "@medusajs/utils"
import { updateProducts, Workflows } from "@medusajs/core-flows"
import { Type } from "class-transformer"
import {
IsArray,
@@ -45,6 +45,7 @@ import {
ProductVariantPricesUpdateReq,
UpdateProductVariantInput,
} from "../../../../types/product-variant"
import { retrieveProduct } from "../../../../utils"
import {
createVariantsTransaction,
revertVariantTransaction,
@@ -292,7 +293,11 @@ export default async (req, res) => {
let rawProduct
if (isMedusaV2Enabled) {
rawProduct = await getProductWithIsolatedProductModule(req, id)
rawProduct = await retrieveProduct(
req.scope,
id,
defaultAdminProductRemoteQueryObject
)
} else {
rawProduct = await productService.retrieve(id, {
select: defaultAdminProductFields,
@@ -305,26 +310,6 @@ export default async (req, res) => {
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
@@ -1,7 +1,10 @@
import { FlagRouter } from "@medusajs/utils"
import { Request } from "express"
import { IdMap, MockManager } from "medusa-test-utils"
import { AdminPostBatchesReq, defaultAdminProductRelations, } from "../../../../api"
import {
AdminPostBatchesReq,
defaultAdminProductRelations,
} from "../../../../api"
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { User } from "../../../../models"
import { BatchJobStatus } from "../../../../types/batch-job"
@@ -117,6 +120,7 @@ describe("Product export strategy", () => {
batchJobService: batchJobServiceMock as any,
productService: productServiceMock as any,
featureFlagRouter: new FlagRouter({}),
remoteQuery: (() => {}) as any,
})
it("should generate the appropriate template", async () => {
@@ -264,6 +268,7 @@ describe("Product export strategy", () => {
productService: productServiceWithNoDataMock as any,
manager: MockManager,
featureFlagRouter: new FlagRouter({}),
remoteQuery: (() => {}) as any,
})
await productExportStrategy.prepareBatchJobForProcessing(
@@ -283,6 +288,7 @@ describe("Product export strategy", () => {
productService: productServiceMock as any,
manager: MockManager,
featureFlagRouter: new FlagRouter({}),
remoteQuery: (() => {}) as any,
})
await productExportStrategy.prepareBatchJobForProcessing(
@@ -381,6 +387,7 @@ describe("Product export strategy with sales Channels", () => {
fileService: fileServiceMock as any,
batchJobService: batchJobServiceMock as any,
productService: productServiceMock as any,
remoteQuery: (() => {}) as any,
featureFlagRouter: new FlagRouter({
[SalesChannelFeatureFlag.key]: true,
}),
@@ -1,8 +1,7 @@
import { EntityManager } from "typeorm"
import { MedusaContainer } from "@medusajs/types"
import { FlagRouter, MedusaV2Flag, createContainerLike } from "@medusajs/utils"
import { humanizeAmount } from "medusa-core-utils"
import { FlagRouter } from "@medusajs/utils"
import { EntityManager } from "typeorm"
import { defaultAdminProductRelations } from "../../../api"
import { AbstractBatchJobStrategy, IFileService } from "../../../interfaces"
import ProductCategoryFeatureFlag from "../../../loaders/feature-flags/product-categories"
@@ -11,19 +10,19 @@ import { Product, ProductVariant } from "../../../models"
import { BatchJobService, ProductService } from "../../../services"
import { BatchJobStatus, CreateBatchJobInput } from "../../../types/batch-job"
import { FindProductConfig } from "../../../types/product"
import { csvCellContentFormatter } from "../../../utils"
import { csvCellContentFormatter, listProducts } from "../../../utils"
import { prepareListQuery } from "../../../utils/get-query-config"
import {
DynamicProductExportDescriptor,
ProductExportBatchJob,
ProductExportBatchJobContext,
ProductExportInjectedDependencies,
ProductExportPriceData,
DynamicProductExportDescriptor,
ProductExportBatchJob,
ProductExportBatchJobContext,
ProductExportInjectedDependencies,
ProductExportPriceData,
} from "./types"
import {
productCategoriesColumnsDefinition,
productColumnsDefinition,
productSalesChannelColumnsDefinition,
productCategoriesColumnsDefinition,
productColumnsDefinition,
productSalesChannelColumnsDefinition,
} from "./types/columns-definition"
export default class ProductExportStrategy extends AbstractBatchJobStrategy {
@@ -37,6 +36,7 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
protected readonly productService_: ProductService
protected readonly fileService_: IFileService
protected readonly featureFlagRouter_: FlagRouter
protected readonly remoteQuery_: any
protected readonly defaultRelations_ = [
...defaultAdminProductRelations,
@@ -68,6 +68,7 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
productService,
fileService,
featureFlagRouter,
remoteQuery,
}: ProductExportInjectedDependencies) {
super({
manager,
@@ -75,8 +76,10 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
productService,
fileService,
featureFlagRouter,
remoteQuery,
})
this.remoteQuery_ = remoteQuery
this.manager_ = manager
this.batchJobService_ = batchJobService
this.productService_ = productService
@@ -135,6 +138,10 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
}
async preProcessBatchJob(batchJobId: string): Promise<void> {
const isMedusaV2Enabled = this.featureFlagRouter_.isFeatureEnabled(
MedusaV2Flag.key
)
return await this.atomicPhase_(async (transactionManager) => {
const batchJob = (await this.batchJobService_
.withTransaction(transactionManager)
@@ -144,12 +151,29 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
const limit = batchJob.context?.list_config?.take ?? this.DEFAULT_LIMIT
const { list_config = {}, filterable_fields = {} } = batchJob.context
const [productList, count] = await this.productService_
.withTransaction(transactionManager)
.listAndCount(filterable_fields, {
...(list_config ?? {}),
take: Math.min(batchJob.context.batch_size ?? Infinity, limit),
} as FindProductConfig)
let productList: Product[] = []
let count = 0
const container = createContainerLike(
this.__container__
) as MedusaContainer
if (isMedusaV2Enabled) {
;[productList, count] = await listProducts(
container,
filterable_fields,
{
...list_config,
take: null,
}
)
} else {
;[productList, count] = await this.productService_
.withTransaction(transactionManager)
.listAndCount(filterable_fields, {
...(list_config ?? {}),
take: Math.min(batchJob.context.batch_size ?? Infinity, limit),
} as FindProductConfig)
}
const productCount = batchJob.context?.batch_size ?? count
let products: Product[] = productList
@@ -162,13 +186,25 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
while (offset < productCount) {
if (!products?.length) {
products = await this.productService_
.withTransaction(transactionManager)
.list(filterable_fields, {
...list_config,
skip: offset,
take: Math.min(productCount - offset, limit),
} as FindProductConfig)
if (isMedusaV2Enabled) {
;[productList, count] = await listProducts(
container,
filterable_fields,
{
...list_config,
skip: offset,
take: Math.min(productCount - offset, limit),
}
)
} else {
products = await this.productService_
.withTransaction(transactionManager)
.list(filterable_fields, {
...list_config,
skip: offset,
take: Math.min(productCount - offset, limit),
} as FindProductConfig)
}
}
const shapeData = this.getProductRelationsDynamicColumnsShape(products)
@@ -669,7 +705,8 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
if (
this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key)
) {
const salesChannelCount = product?.sales_channels?.length ?? 0
const salesChannelCount =
(product as Product)?.sales_channels?.length ?? 0
salesChannelsColumnCount = Math.max(
salesChannelsColumnCount,
salesChannelCount
@@ -1,8 +1,14 @@
/* eslint-disable valid-jsdoc */
import { computerizeAmount, MedusaError } from "medusa-core-utils"
import {
CreateProductVariants,
UpdateProductVariants,
createProducts,
updateProducts,
} from "@medusajs/core-flows"
import { ProductWorkflow } from "@medusajs/types"
import { FlagRouter, MedusaV2Flag, promiseAll } from "@medusajs/utils"
import { MedusaError, computerizeAmount } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { FlagRouter, promiseAll } from "@medusajs/utils"
import { AbstractBatchJobStrategy, IFileService } from "../../../interfaces"
import ProductCategoryFeatureFlag from "../../../loaders/feature-flags/product-categories"
import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels"
@@ -109,7 +115,6 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
})
this.featureFlagRouter_ = featureFlagRouter
this.manager_ = manager
this.fileService_ = fileService
this.batchJobService_ = batchJobService
@@ -168,7 +173,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
const variantsUpdate: TParsedProductImportRowData[] = []
for (const row of csvData) {
if ((row["variant.prices"] as Record<string, any>[]).length) {
if ((row["variant.prices"] as Record<string, any>[])?.length) {
await this.prepareVariantPrices(row)
}
@@ -278,6 +283,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
let totalOperationCount = 0
const operationsCounts = {}
Object.keys(ops).forEach((key) => {
operationsCounts[key] = ops[key].length
totalOperationCount += ops[key].length
@@ -417,6 +423,10 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
return
}
const isMedusaV2Enabled = this.featureFlagRouter_.isFeatureEnabled(
MedusaV2Flag.key
)
const transactionManager = this.transactionManager_ ?? this.manager_
const productOps = await this.downloadImportOpsFile(
@@ -468,10 +478,27 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
)
}
// TODO: we should only pass the expected data and should not have to cast the entire object. Here we are passing everything contained in productData
await productServiceTx.create(
productData as unknown as CreateProductInput
)
if (isMedusaV2Enabled) {
const createProductWorkflow = createProducts(this.__container__)
const input = {
products: [
productData,
] as unknown as ProductWorkflow.CreateProductInputDTO[],
}
await createProductWorkflow.run({
input,
context: {
manager: transactionManager,
},
})
} else {
// TODO: we should only pass the expected data and should not have to cast the entire object. Here we are passing everything contained in productData
await productServiceTx.create(
productData as unknown as CreateProductInput
)
}
} catch (e) {
ProductImportStrategy.throwDescriptiveError(productOp, e.message)
}
@@ -490,6 +517,10 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
return
}
const isMedusaV2Enabled = this.featureFlagRouter_.isFeatureEnabled(
MedusaV2Flag.key
)
const transactionManager = this.transactionManager_ ?? this.manager_
const productOps = await this.downloadImportOpsFile(
batchJob,
@@ -541,11 +572,31 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
)
}
// TODO: we should only pass the expected data. Here we are passing everything contained in productData
await productServiceTx.update(
productOp["product.id"] as string,
productData
)
if (isMedusaV2Enabled) {
const updateProductWorkflow = updateProducts(this.__container__)
const input = {
products: [
{
id: productOp["product.id"] as string,
...productData,
},
],
}
await updateProductWorkflow.run({
input,
context: {
manager: transactionManager,
},
})
} else {
// TODO: we should only pass the expected data. Here we are passing everything contained in productData
await productServiceTx.update(
productOp["product.id"] as string,
productData
)
}
} catch (e) {
ProductImportStrategy.throwDescriptiveError(productOp, e.message)
}
@@ -565,6 +616,10 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
return
}
const isMedusaV2Enabled = this.featureFlagRouter_.isFeatureEnabled(
MedusaV2Flag.key
)
const transactionManager = this.transactionManager_ ?? this.manager_
const variantOps = await this.downloadImportOpsFile(
@@ -600,9 +655,30 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
delete variant.id
delete variant.product
await this.productVariantService_
.withTransaction(transactionManager)
.create(product!, variant as unknown as CreateProductVariantInput)
if (isMedusaV2Enabled) {
const createProductVariantsWorkflow =
CreateProductVariants.createProductVariants(this.__container__)
const input: ProductWorkflow.CreateProductVariantsWorkflowInputDTO = {
productVariants: [
{
...variant,
product_id: product.id,
},
],
}
await createProductVariantsWorkflow.run({
input,
context: {
manager: transactionManager,
},
})
} else {
await this.productVariantService_
.withTransaction(transactionManager)
.create(product!, variant as unknown as CreateProductVariantInput)
}
await this.updateProgress(batchJob.id)
} catch (e) {
@@ -621,6 +697,10 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
return
}
const isMedusaV2Enabled = this.featureFlagRouter_.isFeatureEnabled(
MedusaV2Flag.key
)
const transactionManager = this.transactionManager_ ?? this.manager_
const variantOps = await this.downloadImportOpsFile(
@@ -637,15 +717,36 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
variantOp["product.handle"] as string
)
await this.prepareVariantOptions(variantOp, product.id)
await this.prepareVariantOptions(variantOp, product.id!)
const updateData = transformVariantData(variantOp)
delete updateData.product
delete updateData["product.handle"]
await this.productVariantService_
.withTransaction(transactionManager)
.update(variantOp["variant.id"] as string, updateData)
if (isMedusaV2Enabled) {
const updateProductVariantsWorkflow =
UpdateProductVariants.updateProductVariants(this.__container__)
const input: ProductWorkflow.UpdateProductVariantsWorkflowInputDTO = {
productVariants: [
{
id: variantOp["variant.id"] as string,
...updateData,
},
],
}
await updateProductVariantsWorkflow.run({
input,
context: {
manager: transactionManager,
},
})
} else {
await this.productVariantService_
.withTransaction(transactionManager)
.update(variantOp["variant.id"] as string, updateData)
}
} catch (e) {
ProductImportStrategy.throwDescriptiveError(variantOp, e.message)
}
@@ -1,3 +1,4 @@
import { RemoteQueryFunction } from "@medusajs/types"
import { FlagRouter } from "@medusajs/utils"
import { FileService } from "medusa-interfaces"
import { EntityManager } from "typeorm"
@@ -5,14 +6,14 @@ import { IFileService } from "../../../../interfaces"
import { CsvSchema, CsvSchemaColumn } from "../../../../interfaces/csv-parser"
import { BatchJob, Product, ProductVariant } from "../../../../models"
import {
BatchJobService,
ProductCategoryService,
ProductCollectionService,
ProductService,
ProductVariantService,
RegionService,
SalesChannelService,
ShippingProfileService,
BatchJobService,
ProductCategoryService,
ProductCollectionService,
ProductService,
ProductVariantService,
RegionService,
SalesChannelService,
ShippingProfileService,
} from "../../../../services"
import { Selector } from "../../../../types/common"
@@ -22,6 +23,7 @@ export type ProductExportInjectedDependencies = {
productService: ProductService
fileService: IFileService
featureFlagRouter: FlagRouter
remoteQuery: RemoteQueryFunction
}
export type ProductExportBatchJobContext = {
+1
View File
@@ -11,6 +11,7 @@ export * from "./is-object"
export * from "./is-string"
export * from "./omit-deep"
export * from "./product-category"
export * from "./queries"
export * from "./remote-query-fetch-data"
export * from "./remove-undefined-properties"
export * from "./set-metadata"
@@ -0,0 +1 @@
export * from "./products"
@@ -0,0 +1,34 @@
import { MedusaContainer, ProductVariantDTO } from "@medusajs/types"
export async function getVariantsFromPriceList(
container: MedusaContainer,
priceListId: string
) {
const remoteQuery = container.resolve("remoteQuery")
const query = {
price_list: {
__args: { id: [priceListId] },
price_set_money_amounts: {
price_set: {
variant_link: { variant: { fields: ["id", "product_id"] } },
},
},
},
}
const priceLists = await remoteQuery(query)
const variants: ProductVariantDTO[] = []
priceLists.forEach((priceList) => {
priceList.price_set_money_amounts?.forEach((psma) => {
const variant = psma.price_set?.variant_link?.variant
if (variant) {
variants.push(variant)
}
})
})
return variants
}
@@ -0,0 +1,3 @@
export * from "./get-variants-from-price-list"
export * from "./list-products"
export * from "./retrieve-product"
@@ -0,0 +1,248 @@
import { MedusaContainer } from "@medusajs/types"
import { MedusaV2Flag, promiseAll } from "@medusajs/utils"
import { PriceListService, SalesChannelService } from "../../../services"
import { getVariantsFromPriceList } from "./get-variants-from-price-list"
export async function listProducts(
container: MedusaContainer,
filterableFields,
listConfig
) {
// TODO: Add support for fields/expands
const remoteQuery = container.resolve("remoteQuery")
const featureFlagRouter = container.resolve("featureFlagRouter")
const productIdsFilter: Set<string> = new Set()
const variantIdsFilter: Set<string> = new Set()
const promises: Promise<void>[] = []
// This is not the best way of handling cross filtering but for now I would say it is fine
const salesChannelIdFilter = filterableFields.sales_channel_id
delete filterableFields.sales_channel_id
if (salesChannelIdFilter) {
const salesChannelService = container.resolve(
"salesChannelService"
) as SalesChannelService
promises.push(
salesChannelService
.listProductIdsBySalesChannelIds(salesChannelIdFilter)
.then((productIdsInSalesChannel) => {
let filteredProductIds =
productIdsInSalesChannel[salesChannelIdFilter]
if (filterableFields.id) {
filterableFields.id = Array.isArray(filterableFields.id)
? filterableFields.id
: [filterableFields.id]
const salesChannelProductIdsSet = new Set(filteredProductIds)
filteredProductIds = filterableFields.id.filter((productId) =>
salesChannelProductIdsSet.has(productId)
)
}
filteredProductIds.map((id) => productIdsFilter.add(id))
})
)
}
const priceListId = filterableFields.price_list_id
delete filterableFields.price_list_id
if (priceListId) {
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
const variants = await getVariantsFromPriceList(container, priceListId)
variants.forEach((pv) => variantIdsFilter.add(pv.id))
} else {
// TODO: it is working but validate the behaviour.
// e.g pricing context properly set.
// At the moment filtering by price list but not having any customer id or
// include discount forces the query to filter with price list id is null
const priceListService = container.resolve(
"priceListService"
) as PriceListService
promises.push(
priceListService
.listPriceListsVariantIdsMap(priceListId)
.then((priceListVariantIdsMap) => {
priceListVariantIdsMap[priceListId].map((variantId) =>
variantIdsFilter.add(variantId)
)
})
)
}
}
const discountConditionId = filterableFields.discount_condition_id
delete filterableFields.discount_condition_id
if (discountConditionId) {
// TODO implement later
}
await promiseAll(promises)
if (productIdsFilter.size > 0) {
filterableFields.id = Array.from(productIdsFilter)
}
if (variantIdsFilter.size > 0) {
filterableFields.variants = { id: Array.from(variantIdsFilter) }
}
const variables = {
filters: filterableFields,
order: listConfig.order,
skip: listConfig.skip,
take: listConfig.take,
}
const query = {
product: {
__args: variables,
...defaultAdminProductRemoteQueryObject,
},
}
const {
rows: products,
metadata: { count },
} = await remoteQuery(query)
products.forEach((product) => {
product.profile_id = product.profile?.id
})
return [products, count]
}
export const defaultAdminProductRemoteQueryObject = {
fields: [
"id",
"title",
"subtitle",
"status",
"external_id",
"description",
"handle",
"is_giftcard",
"discountable",
"thumbnail",
"collection_id",
"type_id",
"weight",
"length",
"height",
"width",
"hs_code",
"origin_country",
"mid_code",
"material",
"created_at",
"updated_at",
"deleted_at",
"metadata",
],
images: {
fields: ["id", "created_at", "updated_at", "deleted_at", "url", "metadata"],
},
tags: {
fields: ["id", "created_at", "updated_at", "deleted_at", "value"],
},
type: {
fields: ["id", "created_at", "updated_at", "deleted_at", "value"],
},
collection: {
fields: ["title", "handle", "id", "created_at", "updated_at", "deleted_at"],
},
categories: {
fields: [
"id",
"name",
"description",
"handle",
"is_active",
"is_internal",
"parent_category_id",
],
},
options: {
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"title",
"product_id",
"metadata",
],
values: {
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"value",
"option_id",
"variant_id",
"metadata",
],
},
},
variants: {
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"title",
"product_id",
"sku",
"barcode",
"ean",
"upc",
"variant_rank",
"inventory_quantity",
"allow_backorder",
"manage_inventory",
"hs_code",
"origin_country",
"mid_code",
"material",
"weight",
"length",
"height",
"width",
"metadata",
],
options: {
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"value",
"option_id",
"variant_id",
"metadata",
],
},
},
profile: {
fields: ["id", "created_at", "updated_at", "deleted_at", "name", "type"],
},
}
@@ -0,0 +1,28 @@
import { MedusaError } from "@medusajs/utils"
export async function retrieveProduct(container, id, remoteQueryObject = {}) {
// TODO: Add support for fields/expands
const remoteQuery = container.resolve("remoteQuery")
const variables = { id }
const query = {
product: {
__args: variables,
...remoteQueryObject,
},
}
const [product] = await remoteQuery(query)
if (!product) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Product with id: ${id} not found`
)
}
product.profile_id = product.profile?.id
return product
}
@@ -1,9 +1,9 @@
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
import { WorkflowDefinition, WorkflowManager } from "./workflow-manager"
import { createContainerLike, createMedusaContainer } from "@medusajs/utils"
import { asValue } from "awilix"
import { DistributedTransaction } from "../transaction"
import { asValue } from "awilix"
import { createMedusaContainer } from "@medusajs/utils"
import { WorkflowDefinition, WorkflowManager } from "./workflow-manager"
export class GlobalWorkflow extends WorkflowManager {
protected static workflows: Map<string, WorkflowDefinition> = new Map()
@@ -16,17 +16,17 @@ export class GlobalWorkflow extends WorkflowManager {
) {
super()
const container = createMedusaContainer()
let container
// Medusa container
if (!Array.isArray(modulesLoaded) && modulesLoaded) {
const cradle = modulesLoaded.cradle
for (const key in cradle) {
container.register(key, asValue(cradle[key]))
if (!("cradle" in modulesLoaded)) {
container = createContainerLike(modulesLoaded)
} else {
container = modulesLoaded
}
}
// Array of modules
else if (modulesLoaded?.length) {
} else if (Array.isArray(modulesLoaded) && modulesLoaded.length) {
container = createMedusaContainer()
for (const mod of modulesLoaded) {
const registrationName = mod.__definition.registrationName
container.register(registrationName, asValue(mod))
@@ -1,5 +1,5 @@
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
import { createMedusaContainer } from "@medusajs/utils"
import { createContainerLike, createMedusaContainer } from "@medusajs/utils"
import { asValue } from "awilix"
import {
DistributedTransaction,
@@ -27,7 +27,7 @@ export class LocalWorkflow {
constructor(
workflowId: string,
modulesLoaded?: LoadedModule[] | MedusaContainer
modulesLoaded: LoadedModule[] | MedusaContainer
) {
const globalWorkflow = WorkflowManager.getWorkflow(workflowId)
if (!globalWorkflow) {
@@ -39,17 +39,17 @@ export class LocalWorkflow {
this.workflow = globalWorkflow
this.handlers = new Map(globalWorkflow.handlers_)
const container = createMedusaContainer()
let container
// Medusa container
if (!Array.isArray(modulesLoaded) && modulesLoaded) {
const cradle = modulesLoaded.cradle
for (const key of Object.keys(cradle ?? {})) {
container.register(key, asValue(cradle[key]))
if (!("cradle" in modulesLoaded)) {
container = createContainerLike(modulesLoaded)
} else {
container = modulesLoaded
}
}
// Array of modules
else if (modulesLoaded?.length) {
} else if (Array.isArray(modulesLoaded) && modulesLoaded.length) {
container = createMedusaContainer()
for (const mod of modulesLoaded) {
const registrationName = mod.__definition.registrationName
container.register(registrationName, asValue(mod))
@@ -67,15 +67,15 @@ import {
MedusaError,
promiseAll,
} from "@medusajs/utils"
import {
CreateProductOptionValueDTO,
UpdateProductOptionValueDTO,
} from "../types/services/product-option-value"
import {
entityNameToLinkableKeysMap,
joinerConfig,
LinkableKeys,
} from "./../joiner-config"
import {
CreateProductOptionValueDTO,
UpdateProductOptionValueDTO,
} from "../types/services/product-option-value"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
@@ -1063,6 +1063,10 @@ export default class ProductModuleService<
const existingProductVariantsMap = new Map<string, ProductVariant[]>(
data.map((productData) => {
if (productData.variants === undefined) {
return [productData.id, []]
}
const productVariantsForProduct = existingProductVariants.filter(
(variant) => variant.product_id === productData.id
)
@@ -4,3 +4,7 @@ export type MedusaContainer = AwilixContainer & {
registerAdd: <T>(name: string, registration: T) => MedusaContainer
createScope: () => MedusaContainer
}
export type ContainerLike = {
resolve<T = unknown>(key: string): T
}
@@ -0,0 +1,9 @@
import { ContainerLike } from "@medusajs/types"
export function createContainerLike(obj): ContainerLike {
return {
resolve(key: string) {
return obj[key]
},
}
}
+1
View File
@@ -2,6 +2,7 @@ export * from "./array-difference"
export * from "./build-query"
export * from "./camel-to-snake-case"
export * from "./container"
export * from "./create-container-like"
export * from "./deduplicate"
export * from "./deep-equal-obj"
export * from "./errors"