feat(medusa): Admin list product with product isolated module (#5046)
**What** First iteration: Admin list products return everything but does not include proper filtering by discount id or price list id. Also, same as the previous pr, the fields/expand is not included depends on https://github.com/medusajs/medusa/pull/5130
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(medusa): admin list product with product isolated module
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { IsNumber, IsOptional, IsString } from "class-validator"
|
import { IsNumber, IsOptional, IsString } from "class-validator"
|
||||||
import {
|
import {
|
||||||
|
PriceListService,
|
||||||
PricingService,
|
PricingService,
|
||||||
ProductService,
|
ProductService,
|
||||||
ProductVariantInventoryService,
|
ProductVariantInventoryService,
|
||||||
@@ -11,6 +12,8 @@ import { IInventoryService } from "@medusajs/types"
|
|||||||
import { PricedProduct } from "../../../../types/pricing"
|
import { PricedProduct } from "../../../../types/pricing"
|
||||||
import { Product } from "../../../../models"
|
import { Product } from "../../../../models"
|
||||||
import { Type } from "class-transformer"
|
import { Type } from "class-transformer"
|
||||||
|
import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain"
|
||||||
|
import { defaultAdminProductRemoteQueryObject } from "./index"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [get] /admin/products
|
* @oas [get] /admin/products
|
||||||
@@ -235,16 +238,33 @@ export default async (req, res) => {
|
|||||||
const salesChannelService: SalesChannelService = req.scope.resolve(
|
const salesChannelService: SalesChannelService = req.scope.resolve(
|
||||||
"salesChannelService"
|
"salesChannelService"
|
||||||
)
|
)
|
||||||
|
const featureFlagRouter = req.scope.resolve("featureFlagRouter")
|
||||||
const pricingService: PricingService = req.scope.resolve("pricingService")
|
const pricingService: PricingService = req.scope.resolve("pricingService")
|
||||||
|
|
||||||
const { skip, take, relations } = req.listConfig
|
const { skip, take, relations } = req.listConfig
|
||||||
|
|
||||||
const manager = req.scope.resolve("manager")
|
let rawProducts
|
||||||
|
let count
|
||||||
|
|
||||||
const [rawProducts, count] = await productService.listAndCount(
|
if (featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key)) {
|
||||||
req.filterableFields,
|
const [products, count_] =
|
||||||
req.listConfig
|
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
|
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<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) {
|
||||||
|
// 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 {
|
export class AdminGetProductsParams extends FilterableProductProps {
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FindOperator, FindOptionsWhere, ILike, In } from "typeorm"
|
import { FindOperator, FindOptionsWhere, ILike, In } from "typeorm"
|
||||||
import { PriceList } from "../models"
|
import { PriceList, ProductVariantMoneyAmount } from "../models"
|
||||||
import { ExtendedFindConfig } from "../types/common"
|
import { ExtendedFindConfig } from "../types/common"
|
||||||
import { dataSource } from "../loaders/database"
|
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_)])
|
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
|
export default PriceListRepository
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { CustomerGroup, PriceList, Product, ProductVariant } from "../models"
|
import { CustomerGroup, PriceList, Product, ProductVariant } from "../models"
|
||||||
import { DeepPartial, EntityManager } from "typeorm"
|
import { DeepPartial, EntityManager } from "typeorm"
|
||||||
import { FindConfig, Selector } from "../types/common"
|
import { FindConfig, Selector } from "../types/common"
|
||||||
import { MedusaError, isDefined } from "medusa-core-utils"
|
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||||
|
|
||||||
import { CustomerGroupService } from "."
|
import { CustomerGroupService } from "."
|
||||||
import { FilterableProductProps } from "../types/product"
|
import { FilterableProductProps } from "../types/product"
|
||||||
@@ -106,6 +106,35 @@ class PriceListService extends TransactionBaseService {
|
|||||||
return priceList
|
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
|
* Creates a Price List
|
||||||
* @param priceListObject - the Price List to create
|
* @param priceListObject - the Price List to create
|
||||||
|
|||||||
Reference in New Issue
Block a user