fix(medusa): category_id and q params for list products endpoint ld work (#4889)

Looks like during an earlier refactor, some of the categories logic wasn't ported over to the handler that works with q params. 

what:

- adds a fix that allows queries to be made on category_id when q param is passed.

Fixes https://github.com/medusajs/medusa/issues/4885
This commit is contained in:
Riqwan Thamir
2023-08-28 11:46:55 +00:00
committed by GitHub
parent 962c907dfa
commit a1110b3438
4 changed files with 162 additions and 46 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
fix(medusa): category_id and q params for list products endpoint should work
@@ -27,6 +27,8 @@ describe("/admin/products [MEDUSA_FF_PRODUCT_CATEGORIES=true]", () => {
let categoryWithoutProduct
let nestedCategoryWithProduct
let nested2CategoryWithProduct
let categoryWithMultipleProducts
const categoryWithMultipleProductsId = "category-with-multiple-products-id"
const nestedCategoryWithProductId = "nested-category-with-product-id"
const nested2CategoryWithProductId = "nested2-category-with-product-id"
const categoryWithProductId = "category-with-product-id"
@@ -68,6 +70,17 @@ describe("/admin/products [MEDUSA_FF_PRODUCT_CATEGORIES=true]", () => {
is_internal: false,
})
categoryWithMultipleProducts = await simpleProductCategoryFactory(
dbConnection,
{
id: categoryWithMultipleProductsId,
name: "category with multiple Products",
products: [{ id: testProductId }, { id: testProduct1Id }],
is_active: true,
is_internal: false,
}
)
nestedCategoryWithProduct = await simpleProductCategoryFactory(
dbConnection,
{
@@ -122,6 +135,33 @@ describe("/admin/products [MEDUSA_FF_PRODUCT_CATEGORIES=true]", () => {
])
})
it("should return a list of products queried by category_id and q", async () => {
const api = useApi()
const productName = "Test product1"
// The other product under this category is with the title "Test product"
// By querying for "Test product1", the "Test product" should not be shown
const params = `category_id[]=${categoryWithMultipleProductsId}&q=${productName}&expand=categories`
const response = await api.get(`/admin/products?${params}`, adminHeaders)
expect(response.status).toEqual(200)
expect(response.data.products).toHaveLength(1)
expect(response.data.products).toEqual([
expect.objectContaining({
id: testProduct1Id,
title: productName,
categories: [
expect.objectContaining({
id: categoryWithMultipleProductsId,
}),
expect.objectContaining({
id: nestedCategoryWithProductId,
}),
],
}),
])
})
it("returns a list of products in product category without category children explicitly set to false", async () => {
const api = useApi()
const params = `category_id[]=${categoryWithProductId}&include_category_children=false`
@@ -43,9 +43,11 @@ describe("/store/products", () => {
let internalCategoryWithProduct
let nestedCategoryWithProduct
let nested2CategoryWithProduct
let categoryWithMultipleProducts
const nestedCategoryWithProductId = "nested-category-with-product-id"
const nested2CategoryWithProductId = "nested2-category-with-product-id"
const categoryWithProductId = "category-with-product-id"
const categoryWithMultipleProductsId = "category-with-multiple-products-id"
const categoryWithoutProductId = "category-without-product-id"
const inactiveCategoryWithProductId = "inactive-category-with-product-id"
const internalCategoryWithProductId = "inactive-category-with-product-id"
@@ -62,6 +64,17 @@ describe("/store/products", () => {
await productSeeder(dbConnection, defaultSalesChannel)
await adminSeeder(dbConnection)
categoryWithMultipleProducts = await simpleProductCategoryFactory(
dbConnection,
{
id: categoryWithMultipleProductsId,
name: "category with multiple Products",
products: [{ id: testProductId }, { id: testProductId1 }],
is_active: true,
is_internal: false,
}
)
categoryWithProduct = await simpleProductCategoryFactory(dbConnection, {
id: categoryWithProductId,
name: "category with Product",
@@ -151,6 +164,32 @@ describe("/store/products", () => {
])
})
it("should return a list of products queried by category_id and q", async () => {
const api = useApi()
const productName = "Test product1"
// The other product under this category is with the title "Test product"
// By querying for "Test product1", the "Test product" should not be shown
const params = `category_id[]=${categoryWithMultipleProductsId}&q=${productName}&expand=categories`
const response = await api.get(`/store/products?${params}`)
expect(response.status).toEqual(200)
expect(response.data.products).toHaveLength(1)
expect(response.data.products).toEqual([
expect.objectContaining({
id: testProductId1,
title: productName,
categories: [
expect.objectContaining({
id: categoryWithMultipleProductsId,
}),
expect.objectContaining({
id: nestedCategoryWithProductId,
}),
],
}),
])
})
it("returns a list of products in product category without category children explicitly set to false", async () => {
const api = useApi()
const params = `category_id[]=${categoryWithProductId}&include_category_children=false`
+78 -46
View File
@@ -1,3 +1,7 @@
import { ExtendedFindConfig } from "@medusajs/types"
import { objectToStringPath } from "@medusajs/utils"
import { cloneDeep } from "lodash"
import { isDefined } from "medusa-core-utils"
import {
Brackets,
FindOperator,
@@ -5,6 +9,7 @@ import {
In,
SelectQueryBuilder,
} from "typeorm"
import { dataSource } from "../loaders/database"
import {
PriceList,
Product,
@@ -12,9 +17,6 @@ import {
ProductTag,
SalesChannel,
} from "../models"
import { dataSource } from "../loaders/database"
import { objectToStringPath } from "@medusajs/utils"
import { ExtendedFindConfig } from "@medusajs/types"
import {
applyOrdering,
getGroupedRelations,
@@ -22,20 +24,21 @@ import {
queryEntityWithIds,
queryEntityWithoutRelations,
} from "../utils/repository"
import { cloneDeep } from "lodash"
export type DefaultWithoutRelations = Omit<
ExtendedFindConfig<Product>,
"relations"
>
type CategoryQueryParams = {
value: string[]
}
export type FindWithoutRelationsOptions = DefaultWithoutRelations & {
where: DefaultWithoutRelations["where"] & {
price_list_id?: FindOperator<PriceList>
sales_channel_id?: FindOperator<SalesChannel>
category_id?: {
value: string[]
}
category_id?: CategoryQueryParams
categories?: FindOptionsWhere<ProductCategory>
tags?: FindOperator<ProductTag>
include_category_children?: boolean
@@ -63,7 +66,7 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
const categoriesQuery = optionsWithoutRelations.where.categories || {}
delete optionsWithoutRelations?.where?.categories
const include_category_children =
const includeCategoryChildren =
optionsWithoutRelations?.where?.include_category_children
delete optionsWithoutRelations?.where?.include_category_children
@@ -115,41 +118,10 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
return
},
async (qb, alias) => {
let categoryIds: string[] = []
if (categoryId) {
categoryIds = categoryId?.value
if (include_category_children) {
const categoryRepository =
this.manager.getTreeRepository(ProductCategory)
const categories = await categoryRepository.find({
where: { id: In(categoryIds) },
})
for (const category of categories) {
const categoryChildren =
await categoryRepository.findDescendantsTree(category)
const getAllIdsRecursively = (
productCategory: ProductCategory
) => {
let result = [productCategory.id]
;(productCategory.category_children || []).forEach(
(child) => {
result = result.concat(getAllIdsRecursively(child))
}
)
return result
}
categoryIds = categoryIds.concat(
getAllIdsRecursively(categoryChildren)
)
}
}
}
const categoryIds: string[] = await this.getCategoryIdsFromInput(
categoryId,
includeCategoryChildren
)
if (categoryIds.length || categoriesQuery) {
const joinScope = {}
@@ -357,7 +329,13 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
const discount_condition_id = option_.where.discount_condition_id
delete option_.where.discount_condition_id
const categoriesQuery = option_.where.categories
const categoryId = option_.where.category_id
delete option_.where.category_id
const includeCategoryChildren = option_?.where?.include_category_children
delete option_?.where?.include_category_children
const categoriesQuery = option_.where.categories || {}
delete option_.where.categories
let qb = this.createQueryBuilder(`${productAlias}`)
@@ -414,11 +392,25 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
}
if (categoriesQuery) {
const joinScope = {}
const categoryIds: string[] = await this.getCategoryIdsFromInput(
categoryId,
includeCategoryChildren
)
if (categoryIds.length) {
Object.assign(joinScope, { id: categoryIds })
}
if (categoriesQuery) {
Object.assign(joinScope, categoriesQuery)
}
this._applyCategoriesQuery(qb, {
alias: productAlias,
categoryAlias: "categories",
where: categoriesQuery,
joinName: "leftJoin",
where: joinScope,
joinName: categoryIds.length ? "innerJoin" : "leftJoin",
})
}
@@ -458,6 +450,46 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
return [orderedProducts, count]
},
async getCategoryIdsFromInput(
categoryId?: CategoryQueryParams,
includeCategoryChildren = false
): Promise<string[]> {
let categoryIds = categoryId?.value
if (!isDefined(categoryIds)) {
return []
}
if (includeCategoryChildren) {
const categoryRepository = this.manager.getTreeRepository(ProductCategory)
const categories = await categoryRepository.find({
where: { id: In(categoryIds) },
})
for (const category of categories) {
const categoryChildren = await categoryRepository.findDescendantsTree(
category
)
categoryIds = categoryIds.concat(
this.getCategoryIdsRecursively(categoryChildren)
)
}
}
return categoryIds
},
getCategoryIdsRecursively(productCategory: ProductCategory) {
let result = [productCategory.id]
;(productCategory.category_children || []).forEach((child) => {
result = result.concat(this.getCategoryIdsRecursively(child))
})
return result
},
async _findWithRelations({
relations = [],
idsOrOptionsWithoutRelations = {