feat(medusa): Nested Categories Admin List Endpoint (#2973)

* chore: added get route for admin categories API

* chore: add tree method to mock repository

* chore: added changeset to the PR

* chore: rename id to productCategoryId in service

* chore: switch cli option to string

* chore: lint fixes, tests for parent category

* chore: move Nested Categories behind feature flag

* chore: use transformQuery hook in api

* chore: add feature flag in migrations

* chore: remove migration FF, fix FF name

* chore: add free text search + count repo function

* chore: added list endpoint for admin

* chore: added changeset for feature

* chore: address pr review comments

* chore: change oas comment

* chore: add nullable parent category filter + test
This commit is contained in:
Riqwan Thamir
2023-01-10 12:52:31 +01:00
committed by GitHub
parent 4a50786fbc
commit f3ced106ad
11 changed files with 535 additions and 20 deletions
@@ -111,4 +111,126 @@ describe("/admin/product-categories", () => {
expect(response.status).toEqual(200)
})
})
describe("GET /admin/product-categories", () => {
beforeEach(async () => {
await adminSeeder(dbConnection)
productCategoryParent = await simpleProductCategoryFactory(dbConnection, {
name: "Mens",
handle: "mens",
})
productCategory = await simpleProductCategoryFactory(dbConnection, {
name: "sweater",
handle: "sweater",
parent_category: productCategoryParent,
is_internal: true,
})
productCategoryChild = await simpleProductCategoryFactory(dbConnection, {
name: "cashmere",
handle: "cashmere",
parent_category: productCategory,
})
})
afterEach(async () => {
const db = useDb()
return await db.teardown()
})
it("gets list of product category with immediate children and parents", async () => {
const api = useApi()
const response = await api.get(
`/admin/product-categories`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(3)
expect(response.data.offset).toEqual(0)
expect(response.data.limit).toEqual(100)
expect(response.data.product_categories).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: productCategoryParent.id,
parent_category: null,
category_children: [
expect.objectContaining({
id: productCategory.id,
})
],
}),
expect.objectContaining({
id: productCategory.id,
parent_category: expect.objectContaining({
id: productCategoryParent.id,
}),
category_children: [
expect.objectContaining({
id: productCategoryChild.id,
})
],
}),
expect.objectContaining({
id: productCategoryChild.id,
parent_category: expect.objectContaining({
id: productCategory.id,
}),
category_children: [],
}),
])
)
})
it("filters based on whitelisted attributes of the data model", async () => {
const api = useApi()
const response = await api.get(
`/admin/product-categories?is_internal=true`,
adminHeaders,
)
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(1)
expect(response.data.product_categories[0].id).toEqual(productCategory.id)
})
it("filters based on free text on name and handle columns", async () => {
const api = useApi()
const response = await api.get(
`/admin/product-categories?q=men`,
adminHeaders,
)
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(1)
expect(response.data.product_categories[0].id).toEqual(productCategoryParent.id)
})
it("filters based on parent category", async () => {
const api = useApi()
const response = await api.get(
`/admin/product-categories?parent_category_id=${productCategoryParent.id}`,
adminHeaders,
).catch(e => e)
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(1)
expect(response.data.product_categories[0].id).toEqual(productCategory.id)
const nullCategoryResponse = await api.get(
`/admin/product-categories?parent_category_id=null`,
adminHeaders,
).catch(e => e)
expect(nullCategoryResponse.status).toEqual(200)
expect(nullCategoryResponse.data.count).toEqual(1)
expect(nullCategoryResponse.data.product_categories[0].id).toEqual(productCategoryParent.id)
})
})
})
@@ -2,6 +2,7 @@ import path from "path"
import { ProductCategory } from "@medusajs/medusa"
import { initDb, useDb } from "../../../helpers/use-db"
import { simpleProductCategoryFactory } from '../../factories'
import { ProductCategoryRepository } from "@medusajs/medusa/dist/repositories/product-category"
describe("Product Categories", () => {
let dbConnection
@@ -30,7 +31,8 @@ describe("Product Categories", () => {
a11 = await simpleProductCategoryFactory(dbConnection, { name: 'a11', handle: 'a11', parent_category: a1 })
a111 = await simpleProductCategoryFactory(dbConnection, { name: 'a111', handle: 'a111', parent_category: a11 })
a12 = await simpleProductCategoryFactory(dbConnection, { name: 'a12', handle: 'a12', parent_category: a1 })
productCategoryRepository = dbConnection.getTreeRepository(ProductCategory)
productCategoryRepository = dbConnection.manager.getCustomRepository(ProductCategoryRepository)
})
it("can fetch all root categories", async () => {
@@ -56,7 +58,6 @@ describe("Product Categories", () => {
])
})
it("can fetch all root descendants of a category", async () => {
const a1Children = await productCategoryRepository.findDescendants(a1)
@@ -76,4 +77,131 @@ describe("Product Categories", () => {
])
})
})
describe("getFreeTextSearchResultsAndCount", () => {
let a1, a11, a111, a12
let productCategoryRepository
beforeEach(async () => {
a1 = await simpleProductCategoryFactory(
dbConnection, {
name: 'skinny jeans',
handle: 'skinny-jeans',
is_active: true
}
)
a11 = await simpleProductCategoryFactory(
dbConnection, {
name: 'winter shirts',
handle: 'winter-shirts',
parent_category: a1,
is_active: true
}
)
a111 = await simpleProductCategoryFactory(
dbConnection, {
name: 'running shoes',
handle: 'running-shoes',
parent_category: a11
}
)
a12 = await simpleProductCategoryFactory(
dbConnection, {
name: 'casual shoes',
handle: 'casual-shoes',
parent_category: a1,
is_internal: true
}
)
productCategoryRepository = dbConnection.manager.getCustomRepository(ProductCategoryRepository)
})
it("fetches all active categories", async () => {
const [ categories, count ] = await productCategoryRepository.getFreeTextSearchResultsAndCount(
{ where: { is_active: true } },
)
expect(count).toEqual(2)
expect(categories).toEqual([
expect.objectContaining({
name: a1.name,
}),
expect.objectContaining({
name: a11.name,
}),
])
})
it("fetches all internal categories", async () => {
const [ categories, count ] = await productCategoryRepository.getFreeTextSearchResultsAndCount(
{ where: { is_internal: true } },
)
expect(count).toEqual(1)
expect(categories).toEqual([
expect.objectContaining({
name: a12.name,
}),
])
})
it("fetches all categories with query shoes", async () => {
const [ categories, count ] = await productCategoryRepository.getFreeTextSearchResultsAndCount(
{ where: {} },
'shoes'
)
expect(count).toEqual(2)
expect(categories).toEqual([
expect.objectContaining({
name: a111.name,
}),
expect.objectContaining({
name: a12.name,
}),
])
})
it("fetches all categories with query casual-", async () => {
const [ categories, count ] = await productCategoryRepository.getFreeTextSearchResultsAndCount(
{ where: {} },
'casual-'
)
expect(count).toEqual(1)
expect(categories).toEqual([
expect.objectContaining({
name: a12.name,
}),
])
})
it("builds relations for categories", async () => {
const [ categories, count ] = await productCategoryRepository.getFreeTextSearchResultsAndCount(
{
where: { id: a11.id },
relations: ['parent_category', 'category_children']
},
)
expect(count).toEqual(1)
expect(categories[0]).toEqual(
expect.objectContaining({
id: a11.id,
parent_category: expect.objectContaining({
id: a1.id,
}),
category_children: [
expect.objectContaining({
id: a111.id,
})
]
})
)
})
})
})