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 authored and GitHub committed 2023-08-28 11:46:55 +00:00
1 parent 962c907dfa
commit a1110b3438
4 files changed
+162 -46

No files matched your search

+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 categoryWithoutProduct
let nestedCategoryWithProduct let nestedCategoryWithProduct
let nested2CategoryWithProduct let nested2CategoryWithProduct
let categoryWithMultipleProducts
const categoryWithMultipleProductsId = "category-with-multiple-products-id"
const nestedCategoryWithProductId = "nested-category-with-product-id" const nestedCategoryWithProductId = "nested-category-with-product-id"
const nested2CategoryWithProductId = "nested2-category-with-product-id" const nested2CategoryWithProductId = "nested2-category-with-product-id"
const categoryWithProductId = "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, 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( nestedCategoryWithProduct = await simpleProductCategoryFactory(
dbConnection, 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 () => { it("returns a list of products in product category without category children explicitly set to false", async () => {
const api = useApi() const api = useApi()
const params = `category_id[]=${categoryWithProductId}&include_category_children=false` const params = `category_id[]=${categoryWithProductId}&include_category_children=false`
@@ -43,9 +43,11 @@ describe("/store/products", () => {
let internalCategoryWithProduct let internalCategoryWithProduct
let nestedCategoryWithProduct let nestedCategoryWithProduct
let nested2CategoryWithProduct let nested2CategoryWithProduct
let categoryWithMultipleProducts
const nestedCategoryWithProductId = "nested-category-with-product-id" const nestedCategoryWithProductId = "nested-category-with-product-id"
const nested2CategoryWithProductId = "nested2-category-with-product-id" const nested2CategoryWithProductId = "nested2-category-with-product-id"
const categoryWithProductId = "category-with-product-id" const categoryWithProductId = "category-with-product-id"
const categoryWithMultipleProductsId = "category-with-multiple-products-id"
const categoryWithoutProductId = "category-without-product-id" const categoryWithoutProductId = "category-without-product-id"
const inactiveCategoryWithProductId = "inactive-category-with-product-id" const inactiveCategoryWithProductId = "inactive-category-with-product-id"
const internalCategoryWithProductId = "inactive-category-with-product-id" const internalCategoryWithProductId = "inactive-category-with-product-id"
@@ -62,6 +64,17 @@ describe("/store/products", () => {
await productSeeder(dbConnection, defaultSalesChannel) await productSeeder(dbConnection, defaultSalesChannel)
await adminSeeder(dbConnection) 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, { categoryWithProduct = await simpleProductCategoryFactory(dbConnection, {
id: categoryWithProductId, id: categoryWithProductId,
name: "category with Product", 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 () => { it("returns a list of products in product category without category children explicitly set to false", async () => {
const api = useApi() const api = useApi()
const params = `category_id[]=${categoryWithProductId}&include_category_children=false` 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 { import {
Brackets, Brackets,
FindOperator, FindOperator,
@@ -5,6 +9,7 @@ import {
In, In,
SelectQueryBuilder, SelectQueryBuilder,
} from "typeorm" } from "typeorm"
import { dataSource } from "../loaders/database"
import { import {
PriceList, PriceList,
Product, Product,
@@ -12,9 +17,6 @@ import {
ProductTag, ProductTag,
SalesChannel, SalesChannel,
} from "../models" } from "../models"
import { dataSource } from "../loaders/database"
import { objectToStringPath } from "@medusajs/utils"
import { ExtendedFindConfig } from "@medusajs/types"
import { import {
applyOrdering, applyOrdering,
getGroupedRelations, getGroupedRelations,
@@ -22,20 +24,21 @@ import {
queryEntityWithIds, queryEntityWithIds,
queryEntityWithoutRelations, queryEntityWithoutRelations,
} from "../utils/repository" } from "../utils/repository"
import { cloneDeep } from "lodash"
export type DefaultWithoutRelations = Omit< export type DefaultWithoutRelations = Omit<
ExtendedFindConfig<Product>, ExtendedFindConfig<Product>,
"relations" "relations"
> >
type CategoryQueryParams = {
value: string[]
}
export type FindWithoutRelationsOptions = DefaultWithoutRelations & { export type FindWithoutRelationsOptions = DefaultWithoutRelations & {
where: DefaultWithoutRelations["where"] & { where: DefaultWithoutRelations["where"] & {
price_list_id?: FindOperator<PriceList> price_list_id?: FindOperator<PriceList>
sales_channel_id?: FindOperator<SalesChannel> sales_channel_id?: FindOperator<SalesChannel>
category_id?: { category_id?: CategoryQueryParams
value: string[]
}
categories?: FindOptionsWhere<ProductCategory> categories?: FindOptionsWhere<ProductCategory>
tags?: FindOperator<ProductTag> tags?: FindOperator<ProductTag>
include_category_children?: boolean include_category_children?: boolean
@@ -63,7 +66,7 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
const categoriesQuery = optionsWithoutRelations.where.categories || {} const categoriesQuery = optionsWithoutRelations.where.categories || {}
delete optionsWithoutRelations?.where?.categories delete optionsWithoutRelations?.where?.categories
const include_category_children = const includeCategoryChildren =
optionsWithoutRelations?.where?.include_category_children optionsWithoutRelations?.where?.include_category_children
delete optionsWithoutRelations?.where?.include_category_children delete optionsWithoutRelations?.where?.include_category_children
@@ -115,41 +118,10 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
return return
}, },
async (qb, alias) => { async (qb, alias) => {
let categoryIds: string[] = [] const categoryIds: string[] = await this.getCategoryIdsFromInput(
if (categoryId) { categoryId,
categoryIds = categoryId?.value includeCategoryChildren
)
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)
)
}
}
}
if (categoryIds.length || categoriesQuery) { if (categoryIds.length || categoriesQuery) {
const joinScope = {} const joinScope = {}
@@ -357,7 +329,13 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
const discount_condition_id = option_.where.discount_condition_id const discount_condition_id = option_.where.discount_condition_id
delete 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 delete option_.where.categories
let qb = this.createQueryBuilder(`${productAlias}`) let qb = this.createQueryBuilder(`${productAlias}`)
@@ -414,11 +392,25 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
} }
if (categoriesQuery) { 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, { this._applyCategoriesQuery(qb, {
alias: productAlias, alias: productAlias,
categoryAlias: "categories", categoryAlias: "categories",
where: categoriesQuery, where: joinScope,
joinName: "leftJoin", joinName: categoryIds.length ? "innerJoin" : "leftJoin",
}) })
} }
@@ -458,6 +450,46 @@ export const ProductRepository = dataSource.getRepository(Product).extend({
return [orderedProducts, count] 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({ async _findWithRelations({
relations = [], relations = [],
idsOrOptionsWithoutRelations = { idsOrOptionsWithoutRelations = {