diff --git a/.changeset/gentle-guests-brush.md b/.changeset/gentle-guests-brush.md new file mode 100644 index 0000000000..67727eae15 --- /dev/null +++ b/.changeset/gentle-guests-brush.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): admin list product with product isolated module diff --git a/packages/medusa/src/api/routes/admin/products/list-products.ts b/packages/medusa/src/api/routes/admin/products/list-products.ts index 4640a87e33..62225e0ad3 100644 --- a/packages/medusa/src/api/routes/admin/products/list-products.ts +++ b/packages/medusa/src/api/routes/admin/products/list-products.ts @@ -1,5 +1,6 @@ import { IsNumber, IsOptional, IsString } from "class-validator" import { + PriceListService, PricingService, ProductService, ProductVariantInventoryService, @@ -11,6 +12,8 @@ import { IInventoryService } from "@medusajs/types" import { PricedProduct } from "../../../../types/pricing" import { Product } from "../../../../models" import { Type } from "class-transformer" +import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain" +import { defaultAdminProductRemoteQueryObject } from "./index" /** * @oas [get] /admin/products @@ -235,16 +238,33 @@ export default async (req, res) => { const salesChannelService: SalesChannelService = req.scope.resolve( "salesChannelService" ) + const featureFlagRouter = req.scope.resolve("featureFlagRouter") const pricingService: PricingService = req.scope.resolve("pricingService") const { skip, take, relations } = req.listConfig - const manager = req.scope.resolve("manager") + let rawProducts + let count - const [rawProducts, count] = await productService.listAndCount( - req.filterableFields, - req.listConfig - ) + if (featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key)) { + const [products, count_] = + await listAndCountProductWithIsolatedProductModule( + req, + req.filterableFields, + req.listConfig + ) + + rawProducts = products + count = count_ + } else { + const [products, count_] = await productService.listAndCount( + req.filterableFields, + req.listConfig + ) + + rawProducts = products + count = count_ + } let products: (Product | PricedProduct)[] = rawProducts @@ -280,6 +300,118 @@ export default async (req, res) => { }) } +async function listAndCountProductWithIsolatedProductModule( + req, + filterableFields, + listConfig +) { + // TODO: Add support for fields/expands + + const remoteQuery = req.scope.resolve("remoteQuery") + + const productIdsFilter: Set = new Set() + const variantIdsFilter: Set = new Set() + + const promises: Promise[] = [] + + // 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) { + // 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 Promise.all(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 class AdminGetProductsParams extends FilterableProductProps { @IsNumber() @IsOptional() diff --git a/packages/medusa/src/repositories/price-list.ts b/packages/medusa/src/repositories/price-list.ts index 27e1914bbd..66b93749ed 100644 --- a/packages/medusa/src/repositories/price-list.ts +++ b/packages/medusa/src/repositories/price-list.ts @@ -1,5 +1,5 @@ import { FindOperator, FindOptionsWhere, ILike, In } from "typeorm" -import { PriceList } from "../models" +import { PriceList, ProductVariantMoneyAmount } from "../models" import { ExtendedFindConfig } from "../types/common" import { dataSource } from "../loaders/database" @@ -54,6 +54,29 @@ export const PriceListRepository = dataSource.getRepository(PriceList).extend({ return await Promise.all([this.find(query_), this.count(query_)]) }, + + async listPriceListsVariantIdsMap( + priceListIds: string | string[] + ): Promise<{ [priceListId: string]: string[] }> { + priceListIds = Array.isArray(priceListIds) ? priceListIds : [priceListIds] + + const data = await this.createQueryBuilder("pl") + .innerJoin("pl.prices", "prices") + .innerJoinAndSelect( + ProductVariantMoneyAmount, + "pvma", + "pvma.money_amount_id = prices.id" + ) + .where("pl.id IN (:...ids)", { ids: priceListIds }) + .execute() + + return data.reduce((acc, curr) => { + acc[curr["pl_id"]] ??= [] + acc[curr["pl_id"]].push(curr["pvma_variant_id"]) + acc[curr["pl_id"]] = [...new Set(acc[curr["pl_id"]])] + return acc + }, {}) + }, }) export default PriceListRepository diff --git a/packages/medusa/src/services/price-list.ts b/packages/medusa/src/services/price-list.ts index b70946d69f..aa3d11ff5c 100644 --- a/packages/medusa/src/services/price-list.ts +++ b/packages/medusa/src/services/price-list.ts @@ -8,7 +8,7 @@ import { import { CustomerGroup, PriceList, Product, ProductVariant } from "../models" import { DeepPartial, EntityManager } from "typeorm" import { FindConfig, Selector } from "../types/common" -import { MedusaError, isDefined } from "medusa-core-utils" +import { isDefined, MedusaError } from "medusa-core-utils" import { CustomerGroupService } from "." import { FilterableProductProps } from "../types/product" @@ -106,6 +106,35 @@ class PriceListService extends TransactionBaseService { return priceList } + async listPriceListsVariantIdsMap( + priceListIds: string | string[] + ): Promise<{ [priceListId: string]: string[] }> { + priceListIds = Array.isArray(priceListIds) ? priceListIds : [priceListIds] + + if (!priceListIds.length) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `"priceListIds" must be defined` + ) + } + + const priceListRepo = this.activeManager_.withRepository( + this.priceListRepo_ + ) + + const priceListsVariantIdsMap = + await priceListRepo.listPriceListsVariantIdsMap(priceListIds) + + if (!Object.keys(priceListsVariantIdsMap)?.length) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `No PriceLists found with ids: ${priceListIds.join(", ")}` + ) + } + + return priceListsVariantIdsMap + } + /** * Creates a Price List * @param priceListObject - the Price List to create