feat: Revamp of product categories (#7695)

* feat: Normalize the categories interface to match standards

* feat: Revamp the product category implementation

* fix: Adjustments to code and tests around product categories
This commit is contained in:
Stevche Radevski
2024-06-13 09:10:12 +02:00
committed by GitHub
parent fbd8eef18b
commit d862d03de0
35 changed files with 1135 additions and 874 deletions

View File

@@ -66,126 +66,126 @@ export const productCategoriesRankData = [
},
]
export const eletronicsCategoriesData = eval(`[
export const eletronicsCategoriesData = [
{
id: "electronics",
name: "Electronics",
parent_category_id: null,
},
{
id: "computers",
name: "Computers & Accessories",
parent_category_id: "electronics",
},
{
id: "desktops",
name: "Desktops",
parent_category_id: "computers",
},
{
id: "gaming-desktops",
name: "Gaming Desktops",
parent_category_id: "desktops",
},
{
id: "office-desktops",
name: "Office Desktops",
parent_category_id: "desktops",
},
{
id: "laptops",
name: "Laptops",
parent_category_id: "computers",
},
{
id: "gaming-laptops",
name: "Gaming Laptops",
parent_category_id: "laptops",
},
{
id: "budget-gaming",
name: "Budget Gaming Laptops",
parent_category_id: "gaming-laptops",
},
{
id: "high-performance",
name: "High Performance Gaming Laptops",
parent_category_id: "gaming-laptops",
},
{
id: "vr-ready",
name: "VR-Ready High Performance Gaming Laptops",
parent_category_id: "high-performance",
},
{
id: "4k-gaming",
name: "4K Gaming Laptops",
parent_category_id: "high-performance",
},
{
id: "ultrabooks",
name: "Ultrabooks",
parent_category_id: "laptops",
},
{
id: "thin-light",
name: "Thin & Light Ultrabooks",
parent_category_id: "ultrabooks",
},
{
id: "convertible-ultrabooks",
name: "Convertible Ultrabooks",
parent_category_id: "ultrabooks",
},
{
id: "touchscreen-ultrabooks",
name: "Touchscreen Ultrabooks",
parent_category_id: "convertible-ultrabooks",
},
{
id: "detachable-ultrabooks",
name: "Detachable Ultrabooks",
parent_category_id: "convertible-ultrabooks",
},
{
id: "mobile",
name: "Mobile Phones & Accessories",
parent_category_id: "electronics",
},
{
id: "smartphones",
name: "Smartphones",
parent_category_id: "mobile",
},
{
id: "android-phones",
name: "Android Phones",
parent_category_id: "smartphones",
},
{
id: "flagship-phones",
name: "Flagship Smartphones",
parent_category_id: "android-phones",
},
{
id: "budget-phones",
name: "Budget Smartphones",
parent_category_id: "android-phones",
},
{
id: "iphones",
name: "iPhones",
parent_category_id: "smartphones",
},
{
id: "pro-phones",
name: "Pro Models",
parent_category_id: "iphones",
},
{
id: "mini-phones",
name: "Mini Models",
parent_category_id: "iphones",
},
]`)
{
id: "computers",
name: "Computers & Accessories",
parent_category_id: "electronics",
},
{
id: "desktops",
name: "Desktops",
parent_category_id: "computers",
},
{
id: "gaming-desktops",
name: "Gaming Desktops",
parent_category_id: "desktops",
},
{
id: "office-desktops",
name: "Office Desktops",
parent_category_id: "desktops",
},
{
id: "laptops",
name: "Laptops",
parent_category_id: "computers",
},
{
id: "gaming-laptops",
name: "Gaming Laptops",
parent_category_id: "laptops",
},
{
id: "budget-gaming",
name: "Budget Gaming Laptops",
parent_category_id: "gaming-laptops",
},
{
id: "high-performance",
name: "High Performance Gaming Laptops",
parent_category_id: "gaming-laptops",
},
{
id: "vr-ready",
name: "VR-Ready High Performance Gaming Laptops",
parent_category_id: "high-performance",
},
{
id: "4k-gaming",
name: "4K Gaming Laptops",
parent_category_id: "high-performance",
},
{
id: "ultrabooks",
name: "Ultrabooks",
parent_category_id: "laptops",
},
{
id: "thin-light",
name: "Thin & Light Ultrabooks",
parent_category_id: "ultrabooks",
},
{
id: "convertible-ultrabooks",
name: "Convertible Ultrabooks",
parent_category_id: "ultrabooks",
},
{
id: "touchscreen-ultrabooks",
name: "Touchscreen Ultrabooks",
parent_category_id: "convertible-ultrabooks",
},
{
id: "detachable-ultrabooks",
name: "Detachable Ultrabooks",
parent_category_id: "convertible-ultrabooks",
},
{
id: "mobile",
name: "Mobile Phones & Accessories",
parent_category_id: "electronics",
},
{
id: "smartphones",
name: "Smartphones",
parent_category_id: "mobile",
},
{
id: "android-phones",
name: "Android Phones",
parent_category_id: "smartphones",
},
{
id: "flagship-phones",
name: "Flagship Smartphones",
parent_category_id: "android-phones",
},
{
id: "budget-phones",
name: "Budget Smartphones",
parent_category_id: "android-phones",
},
{
id: "iphones",
name: "iPhones",
parent_category_id: "smartphones",
},
{
id: "pro-phones",
name: "Pro Models",
parent_category_id: "iphones",
},
{
id: "mini-phones",
name: "Mini Models",
parent_category_id: "iphones",
},
]

View File

@@ -1,31 +0,0 @@
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { ProductCategory } from "@models"
export async function createProductCategories(
manager: SqlEntityManager,
categoriesData: any[]
): Promise<ProductCategory[]> {
const categories: ProductCategory[] = []
for (let categoryData of categoriesData) {
let categoryDataClone = { ...categoryData }
let parentCategory: ProductCategory | null = null
const parentCategoryId = categoryDataClone.parent_category_id as string
delete categoryDataClone.parent_category_id
if (parentCategoryId) {
parentCategory = await manager.findOne(ProductCategory, parentCategoryId)
}
const category = manager.create(ProductCategory, {
...categoryDataClone,
parent_category: parentCategory,
})
categories.push(category)
}
await manager.persistAndFlush(categories)
return categories
}

View File

@@ -2,7 +2,6 @@ import { ProductCategoryService } from "@services"
import { Modules } from "@medusajs/modules-sdk"
import { moduleIntegrationTestRunner } from "medusa-test-utils"
import { createProductCategories } from "../__fixtures__/product-category"
import {
eletronicsCategoriesData,
productCategoriesData,
@@ -28,10 +27,9 @@ moduleIntegrationTestRunner<Service>({
describe("list", () => {
beforeEach(async () => {
await createProductCategories(
MikroOrmWrapper.forkManager(),
productCategoriesData
)
for (const entry of productCategoriesData) {
await service.create([entry])
}
})
it("lists all product categories", async () => {
@@ -131,33 +129,33 @@ moduleIntegrationTestRunner<Service>({
expect.objectContaining({
id: "category-0",
handle: "category-0",
mpath: "category-0.",
mpath: "category-0",
parent_category_id: null,
category_children: [
expect.objectContaining({
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1.",
mpath: "category-0.category-1",
parent_category_id: "category-0",
category_children: [
expect.objectContaining({
id: "category-1-a",
handle: "category-1-a",
mpath: "category-0.category-1.category-1-a.",
mpath: "category-0.category-1.category-1-a",
parent_category_id: "category-1",
category_children: [],
}),
expect.objectContaining({
id: "category-1-b",
handle: "category-1-b",
mpath: "category-0.category-1.category-1-b.",
mpath: "category-0.category-1.category-1-b",
parent_category_id: "category-1",
category_children: [
expect.objectContaining({
id: "category-1-b-1",
handle: "category-1-b-1",
mpath:
"category-0.category-1.category-1-b.category-1-b-1.",
"category-0.category-1.category-1-b.category-1-b-1",
parent_category_id: "category-1-b",
category_children: [],
}),
@@ -189,20 +187,20 @@ moduleIntegrationTestRunner<Service>({
{
id: "category-1-a",
handle: "category-1-a",
mpath: "category-0.category-1.category-1-a.",
mpath: "category-0.category-1.category-1-a",
parent_category_id: "category-1",
category_children: [],
},
{
id: "category-1-b",
handle: "category-1-b",
mpath: "category-0.category-1.category-1-b.",
mpath: "category-0.category-1.category-1-b",
parent_category_id: "category-1",
category_children: [
expect.objectContaining({
id: "category-1-b-1",
handle: "category-1-b-1",
mpath: "category-0.category-1.category-1-b.category-1-b-1.",
mpath: "category-0.category-1.category-1-b.category-1-b-1",
parent_category_id: "category-1-b",
category_children: [],
}),
@@ -212,10 +210,9 @@ moduleIntegrationTestRunner<Service>({
})
it("includes the entire list of parents when include_ancestors_tree is true", async () => {
await createProductCategories(
MikroOrmWrapper.forkManager(),
eletronicsCategoriesData
)
for (const entry of eletronicsCategoriesData) {
await service.create([entry])
}
const productCategoryResults = await service.list(
{
@@ -236,34 +233,34 @@ moduleIntegrationTestRunner<Service>({
id: "4k-gaming",
handle: "4k-gaming-laptops",
mpath:
"electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming.",
"electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming",
parent_category_id: "high-performance",
parent_category: {
id: "high-performance",
parent_category_id: "gaming-laptops",
handle: "high-performance-gaming-laptops",
mpath:
"electronics.computers.laptops.gaming-laptops.high-performance.",
"electronics.computers.laptops.gaming-laptops.high-performance",
parent_category: {
id: "gaming-laptops",
handle: "gaming-laptops",
mpath: "electronics.computers.laptops.gaming-laptops.",
mpath: "electronics.computers.laptops.gaming-laptops",
parent_category_id: "laptops",
parent_category: {
id: "laptops",
parent_category_id: "computers",
handle: "laptops",
mpath: "electronics.computers.laptops.",
mpath: "electronics.computers.laptops",
parent_category: {
id: "computers",
handle: "computers-&-accessories",
mpath: "electronics.computers.",
mpath: "electronics.computers",
parent_category_id: "electronics",
parent_category: {
id: "electronics",
parent_category_id: null,
handle: "electronics",
mpath: "electronics.",
mpath: "electronics",
parent_category: null,
},
},
@@ -275,10 +272,9 @@ moduleIntegrationTestRunner<Service>({
})
it("includes the entire list of descendants when include_descendants_tree is true", async () => {
await createProductCategories(
MikroOrmWrapper.forkManager(),
eletronicsCategoriesData
)
for (const entry of eletronicsCategoriesData) {
await service.create([entry])
}
const productCategoryResults = await service.list(
{
@@ -298,14 +294,14 @@ moduleIntegrationTestRunner<Service>({
{
id: "gaming-laptops",
handle: "gaming-laptops",
mpath: "electronics.computers.laptops.gaming-laptops.",
mpath: "electronics.computers.laptops.gaming-laptops",
parent_category_id: "laptops",
category_children: [
{
id: "budget-gaming",
handle: "budget-gaming-laptops",
mpath:
"electronics.computers.laptops.gaming-laptops.budget-gaming.",
"electronics.computers.laptops.gaming-laptops.budget-gaming",
parent_category_id: "gaming-laptops",
category_children: [],
},
@@ -313,14 +309,14 @@ moduleIntegrationTestRunner<Service>({
id: "high-performance",
handle: "high-performance-gaming-laptops",
mpath:
"electronics.computers.laptops.gaming-laptops.high-performance.",
"electronics.computers.laptops.gaming-laptops.high-performance",
parent_category_id: "gaming-laptops",
category_children: expect.arrayContaining([
{
id: "4k-gaming",
handle: "4k-gaming-laptops",
mpath:
"electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming.",
"electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming",
parent_category_id: "high-performance",
category_children: [],
},
@@ -328,7 +324,7 @@ moduleIntegrationTestRunner<Service>({
id: "vr-ready",
handle: "vr-ready-high-performance-gaming-laptops",
mpath:
"electronics.computers.laptops.gaming-laptops.high-performance.vr-ready.",
"electronics.computers.laptops.gaming-laptops.high-performance.vr-ready",
parent_category_id: "high-performance",
category_children: [],
},
@@ -340,10 +336,9 @@ moduleIntegrationTestRunner<Service>({
})
it("includes the entire list of descendants an parents when include_descendants_tree and include_ancestors_tree are true", async () => {
await createProductCategories(
MikroOrmWrapper.forkManager(),
eletronicsCategoriesData
)
for (const entry of eletronicsCategoriesData) {
await service.create([entry])
}
const productCategoryResults = await service.list(
{
@@ -364,22 +359,22 @@ moduleIntegrationTestRunner<Service>({
{
id: "gaming-laptops",
handle: "gaming-laptops",
mpath: "electronics.computers.laptops.gaming-laptops.",
mpath: "electronics.computers.laptops.gaming-laptops",
parent_category_id: "laptops",
parent_category: {
id: "laptops",
handle: "laptops",
mpath: "electronics.computers.laptops.",
mpath: "electronics.computers.laptops",
parent_category_id: "computers",
parent_category: {
id: "computers",
handle: "computers-&-accessories",
mpath: "electronics.computers.",
mpath: "electronics.computers",
parent_category_id: "electronics",
parent_category: {
id: "electronics",
handle: "electronics",
mpath: "electronics.",
mpath: "electronics",
parent_category_id: null,
parent_category: null,
},
@@ -390,14 +385,14 @@ moduleIntegrationTestRunner<Service>({
id: "budget-gaming",
handle: "budget-gaming-laptops",
mpath:
"electronics.computers.laptops.gaming-laptops.budget-gaming.",
"electronics.computers.laptops.gaming-laptops.budget-gaming",
parent_category_id: "gaming-laptops",
},
{
id: "high-performance",
handle: "high-performance-gaming-laptops",
mpath:
"electronics.computers.laptops.gaming-laptops.high-performance.",
"electronics.computers.laptops.gaming-laptops.high-performance",
parent_category_id: "gaming-laptops",
},
],
@@ -424,17 +419,17 @@ moduleIntegrationTestRunner<Service>({
{
id: "category-1-a",
handle: "category-1-a",
mpath: "category-0.category-1.category-1-a.",
mpath: "category-0.category-1.category-1-a",
parent_category_id: "category-1",
parent_category: {
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1.",
mpath: "category-0.category-1",
parent_category_id: "category-0",
parent_category: {
id: "category-0",
handle: "category-0",
mpath: "category-0.",
mpath: "category-0",
parent_category_id: null,
parent_category: null,
},
@@ -443,17 +438,17 @@ moduleIntegrationTestRunner<Service>({
{
id: "category-1-b",
handle: "category-1-b",
mpath: "category-0.category-1.category-1-b.",
mpath: "category-0.category-1.category-1-b",
parent_category_id: "category-1",
parent_category: {
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1.",
mpath: "category-0.category-1",
parent_category_id: "category-0",
parent_category: {
id: "category-0",
handle: "category-0",
mpath: "category-0.",
mpath: "category-0",
parent_category_id: null,
parent_category: null,
},
@@ -482,17 +477,17 @@ moduleIntegrationTestRunner<Service>({
{
id: "category-1-a",
handle: "category-1-a",
mpath: "category-0.category-1.category-1-a.",
mpath: "category-0.category-1.category-1-a",
parent_category_id: "category-1",
parent_category: {
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1.",
mpath: "category-0.category-1",
parent_category_id: "category-0",
parent_category: {
id: "category-0",
handle: "category-0",
mpath: "category-0.",
mpath: "category-0",
parent_category_id: null,
parent_category: null,
},
@@ -502,17 +497,17 @@ moduleIntegrationTestRunner<Service>({
{
id: "category-1-b",
handle: "category-1-b",
mpath: "category-0.category-1.category-1-b.",
mpath: "category-0.category-1.category-1-b",
parent_category_id: "category-1",
parent_category: {
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1.",
mpath: "category-0.category-1",
parent_category_id: "category-0",
parent_category: {
id: "category-0",
handle: "category-0",
mpath: "category-0.",
mpath: "category-0",
parent_category_id: null,
parent_category: null,
},
@@ -521,7 +516,7 @@ moduleIntegrationTestRunner<Service>({
{
id: "category-1-b-1",
handle: "category-1-b-1",
mpath: "category-0.category-1.category-1-b.category-1-b-1.",
mpath: "category-0.category-1.category-1-b.category-1-b-1",
parent_category_id: "category-1-b",
},
],
@@ -549,19 +544,19 @@ moduleIntegrationTestRunner<Service>({
expect.objectContaining({
id: "category-0",
handle: "category-0",
mpath: "category-0.",
mpath: "category-0",
parent_category_id: null,
category_children: [
expect.objectContaining({
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1.",
mpath: "category-0.category-1",
parent_category_id: "category-0",
category_children: [
expect.objectContaining({
id: "category-1-a",
handle: "category-1-a",
mpath: "category-0.category-1.category-1-a.",
mpath: "category-0.category-1.category-1-a",
parent_category_id: "category-1",
category_children: [],
}),
@@ -577,10 +572,9 @@ moduleIntegrationTestRunner<Service>({
const categoryOneId = "category-1"
beforeEach(async () => {
await createProductCategories(
MikroOrmWrapper.forkManager(),
productCategoriesData
)
for (const entry of productCategoriesData) {
await service.create([entry])
}
})
it("should return category for the given id", async () => {
@@ -659,10 +653,9 @@ moduleIntegrationTestRunner<Service>({
describe("listAndCount", () => {
beforeEach(async () => {
await createProductCategories(
MikroOrmWrapper.forkManager(),
productCategoriesData
)
for (const entry of productCategoriesData) {
await service.create([entry])
}
})
it("should return categories and count based on take and skip", async () => {
@@ -800,33 +793,33 @@ moduleIntegrationTestRunner<Service>({
expect.objectContaining({
id: "category-0",
handle: "category-0",
mpath: "category-0.",
mpath: "category-0",
parent_category_id: null,
category_children: [
expect.objectContaining({
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1.",
mpath: "category-0.category-1",
parent_category_id: "category-0",
category_children: [
expect.objectContaining({
id: "category-1-a",
handle: "category-1-a",
mpath: "category-0.category-1.category-1-a.",
mpath: "category-0.category-1.category-1-a",
parent_category_id: "category-1",
category_children: [],
}),
expect.objectContaining({
id: "category-1-b",
handle: "category-1-b",
mpath: "category-0.category-1.category-1-b.",
mpath: "category-0.category-1.category-1-b",
parent_category_id: "category-1",
category_children: [
expect.objectContaining({
id: "category-1-b-1",
handle: "category-1-b-1",
mpath:
"category-0.category-1.category-1-b.category-1-b-1.",
"category-0.category-1.category-1-b.category-1-b-1",
parent_category_id: "category-1-b",
category_children: [],
}),
@@ -861,19 +854,19 @@ moduleIntegrationTestRunner<Service>({
expect.objectContaining({
id: "category-0",
handle: "category-0",
mpath: "category-0.",
mpath: "category-0",
parent_category_id: null,
category_children: [
expect.objectContaining({
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1.",
mpath: "category-0.category-1",
parent_category_id: "category-0",
category_children: [
expect.objectContaining({
id: "category-1-a",
handle: "category-1-a",
mpath: "category-0.category-1.category-1-a.",
mpath: "category-0.category-1.category-1-a",
parent_category_id: "category-1",
category_children: [],
}),
@@ -887,10 +880,12 @@ moduleIntegrationTestRunner<Service>({
describe("create", () => {
it("should create a category successfully", async () => {
await service.create({
name: "New Category",
parent_category_id: null,
})
await service.create([
{
name: "New Category",
parent_category_id: null,
},
])
const [productCategory] = await service.list(
{
@@ -910,16 +905,17 @@ moduleIntegrationTestRunner<Service>({
})
it("should append rank from an existing category depending on parent", async () => {
await service.create({
name: "New Category",
parent_category_id: null,
rank: 0,
})
await service.create({
name: "New Category 2",
parent_category_id: null,
})
await service.create([
{
name: "New Category",
parent_category_id: null,
rank: 0,
},
{
name: "New Category 2",
parent_category_id: null,
},
])
const [productCategoryNew] = await service.list(
{
@@ -930,17 +926,19 @@ moduleIntegrationTestRunner<Service>({
}
)
expect(productCategoryNew).toEqual(
expect(JSON.parse(JSON.stringify(productCategoryNew))).toEqual(
expect.objectContaining({
name: "New Category 2",
rank: 1,
})
)
await service.create({
name: "New Category 2.1",
parent_category_id: productCategoryNew.id,
})
await service.create([
{
name: "New Category 2.1",
parent_category_id: productCategoryNew.id,
},
])
const [productCategoryWithParent] = await service.list(
{
@@ -971,10 +969,10 @@ moduleIntegrationTestRunner<Service>({
let categories
beforeEach(async () => {
categories = await createProductCategories(
MikroOrmWrapper.forkManager(),
productCategoriesRankData
)
categories = []
for (const entry of productCategoriesRankData) {
categories.push((await service.create([entry]))[0])
}
productCategoryZero = categories[0]
productCategoryOne = categories[1]
@@ -985,9 +983,12 @@ moduleIntegrationTestRunner<Service>({
})
it("should update the name of the category successfully", async () => {
await service.update(productCategoryZero.id, {
name: "New Category",
})
await service.update([
{
id: productCategoryZero.id,
name: "New Category",
},
])
const productCategory = await service.retrieve(
productCategoryZero.id,
@@ -1003,9 +1004,12 @@ moduleIntegrationTestRunner<Service>({
let error
try {
await service.update("does-not-exist", {
name: "New Category",
})
await service.update([
{
id: "does-not-exist",
name: "New Category",
},
])
} catch (e) {
error = e
}
@@ -1016,9 +1020,12 @@ moduleIntegrationTestRunner<Service>({
})
it("should reorder rank successfully in the same parent", async () => {
await service.update(productCategoryTwo.id, {
rank: 0,
})
await service.update([
{
id: productCategoryTwo.id,
rank: 0,
},
])
const productCategories = await service.list(
{
@@ -1048,10 +1055,13 @@ moduleIntegrationTestRunner<Service>({
})
it("should reorder rank successfully when changing parent", async () => {
await service.update(productCategoryTwo.id, {
rank: 0,
parent_category_id: productCategoryZero.id,
})
await service.update([
{
id: productCategoryTwo.id,
rank: 0,
parent_category_id: productCategoryZero.id,
},
])
const productCategories = await service.list(
{
@@ -1085,10 +1095,13 @@ moduleIntegrationTestRunner<Service>({
})
it("should reorder rank successfully when changing parent and in first position", async () => {
await service.update(productCategoryTwo.id, {
rank: 0,
parent_category_id: productCategoryZero.id,
})
await service.update([
{
id: productCategoryTwo.id,
rank: 0,
parent_category_id: productCategoryZero.id,
},
])
const productCategories = await service.list(
{
@@ -1129,10 +1142,10 @@ moduleIntegrationTestRunner<Service>({
let categories
beforeEach(async () => {
categories = await createProductCategories(
MikroOrmWrapper.forkManager(),
productCategoriesRankData
)
categories = []
for (const entry of productCategoriesRankData) {
categories.push((await service.create([entry]))[0])
}
productCategoryZero = categories[0]
productCategoryOne = categories[1]
@@ -1143,7 +1156,7 @@ moduleIntegrationTestRunner<Service>({
let error
try {
await service.delete("does-not-exist")
await service.delete(["does-not-exist"])
} catch (e) {
error = e
}
@@ -1157,7 +1170,7 @@ moduleIntegrationTestRunner<Service>({
let error
try {
await service.delete(productCategoryZero.id)
await service.delete([productCategoryZero.id])
} catch (e) {
error = e
}
@@ -1168,7 +1181,7 @@ moduleIntegrationTestRunner<Service>({
})
it("should reorder siblings rank successfully on deleting", async () => {
await service.delete(productCategoryOne.id)
await service.delete([productCategoryOne.id])
const productCategories = await service.list(
{

View File

@@ -6,7 +6,6 @@ import {
MockEventBusService,
moduleIntegrationTestRunner,
} from "medusa-test-utils"
import { createProductCategories } from "../../__fixtures__/product-category"
import { productCategoriesRankData } from "../../__fixtures__/product-category/data"
jest.setTimeout(30000)
@@ -52,18 +51,12 @@ moduleIntegrationTestRunner<IProductModuleService>({
},
]
productCategories = await createProductCategories(
testManager,
productCategoriesData
)
productCategories = []
for (const entry of productCategoriesData) {
productCategories.push(await service.createCategories(entry))
}
productCategoryOne = productCategories[0]
productCategoryTwo = productCategories[1]
await testManager.persistAndFlush([
productCategoryOne,
productCategoryTwo,
])
})
afterEach(async () => {
@@ -260,7 +253,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
describe("createCategory", () => {
it("should create a category successfully", async () => {
await service.createCategory({
await service.createCategories({
name: "New Category",
parent_category_id: productCategoryOne.id,
})
@@ -285,26 +278,28 @@ moduleIntegrationTestRunner<IProductModuleService>({
it("should emit events through event bus", async () => {
const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
const category = await service.createCategory({
const category = await service.createCategories({
name: "New Category",
parent_category_id: productCategoryOne.id,
})
expect(eventBusSpy).toHaveBeenCalledTimes(1)
expect(eventBusSpy).toHaveBeenCalledWith({
data: { id: category.id },
eventName: "product-category.created",
})
expect(eventBusSpy).toHaveBeenCalledWith([
expect.objectContaining({
data: { id: category.id },
eventName: "productService.product-category.created",
}),
])
})
it("should append rank from an existing category depending on parent", async () => {
await service.createCategory({
await service.createCategories({
name: "New Category",
parent_category_id: productCategoryOne.id,
rank: 0,
})
await service.createCategory({
await service.createCategories({
name: "New Category 2",
parent_category_id: productCategoryOne.id,
})
@@ -325,7 +320,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
})
)
await service.createCategory({
await service.createCategories({
name: "New Category 2.1",
parent_category_id: productCategoryNew.id,
})
@@ -359,12 +354,10 @@ moduleIntegrationTestRunner<IProductModuleService>({
let categories
beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager()
categories = await createProductCategories(
testManager,
productCategoriesRankData
)
categories = []
for (const entry of productCategoriesRankData) {
categories.push(await service.createCategories(entry))
}
productCategoryZero = categories[0]
productCategoryOne = categories[1]
@@ -376,19 +369,23 @@ moduleIntegrationTestRunner<IProductModuleService>({
it("should emit events through event bus", async () => {
const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
await service.updateCategory(productCategoryZero.id, {
eventBusSpy.mockClear()
await service.updateCategories(productCategoryZero.id, {
name: "New Category",
})
expect(eventBusSpy).toHaveBeenCalledTimes(1)
expect(eventBusSpy).toHaveBeenCalledWith({
data: { id: productCategoryZero.id },
eventName: "product-category.updated",
})
expect(eventBusSpy).toHaveBeenCalledWith([
expect.objectContaining({
data: { id: productCategoryZero.id },
eventName: "productService.product-category.updated",
}),
])
})
it("should update the name of the category successfully", async () => {
await service.updateCategory(productCategoryZero.id, {
await service.updateCategories(productCategoryZero.id, {
name: "New Category",
})
@@ -406,7 +403,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
let error
try {
await service.updateCategory("does-not-exist", {
await service.updateCategories("does-not-exist", {
name: "New Category",
})
} catch (e) {
@@ -414,12 +411,12 @@ moduleIntegrationTestRunner<IProductModuleService>({
}
expect(error.message).toEqual(
`ProductCategory not found ({ id: 'does-not-exist' })`
`ProductCategory with id: does-not-exist was not found`
)
})
it("should reorder rank successfully in the same parent", async () => {
await service.updateCategory(productCategoryTwo.id, {
await service.updateCategories(productCategoryTwo.id, {
rank: 0,
})
@@ -451,7 +448,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
})
it("should reorder rank successfully when changing parent", async () => {
await service.updateCategory(productCategoryTwo.id, {
await service.updateCategories(productCategoryTwo.id, {
rank: 0,
parent_category_id: productCategoryZero.id,
})
@@ -488,7 +485,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
})
it("should reorder rank successfully when changing parent and in first position", async () => {
await service.updateCategory(productCategoryTwo.id, {
await service.updateCategories(productCategoryTwo.id, {
rank: 0,
parent_category_id: productCategoryZero.id,
})
@@ -532,34 +529,37 @@ moduleIntegrationTestRunner<IProductModuleService>({
let categories
beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager()
categories = await createProductCategories(
testManager,
productCategoriesRankData
)
categories = []
for (const entry of productCategoriesRankData) {
categories.push(await service.createCategories(entry))
}
productCategoryZero = categories[0]
productCategoryOne = categories[1]
productCategoryTwo = categories[2]
})
// TODO: Normalize delete events as well
it("should emit events through event bus", async () => {
const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
await service.deleteCategory(productCategoryOne.id)
eventBusSpy.mockClear()
await service.deleteCategories([productCategoryOne.id])
expect(eventBusSpy).toHaveBeenCalledTimes(1)
expect(eventBusSpy).toHaveBeenCalledWith({
data: { id: productCategoryOne.id },
eventName: "product-category.deleted",
})
expect(eventBusSpy).toHaveBeenCalledWith([
expect.objectContaining({
data: { id: productCategoryOne.id },
eventName: "product-category.deleted",
}),
])
})
it("should throw an error when an id does not exist", async () => {
let error
try {
await service.deleteCategory("does-not-exist")
await service.deleteCategories(["does-not-exist"])
} catch (e) {
error = e
}
@@ -573,7 +573,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
let error
try {
await service.deleteCategory(productCategoryZero.id)
await service.deleteCategories([productCategoryZero.id])
} catch (e) {
error = e
}
@@ -584,7 +584,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
})
it("should reorder siblings rank successfully on deleting", async () => {
await service.deleteCategory(productCategoryOne.id)
await service.deleteCategories([productCategoryOne.id])
const productCategories = await service.listCategories(
{

View File

@@ -1,5 +1,5 @@
import { Modules } from "@medusajs/modules-sdk"
import { IProductModuleService } from "@medusajs/types"
import { IProductModuleService, ProductCategoryDTO } from "@medusajs/types"
import { kebabCase, ProductStatus } from "@medusajs/utils"
import {
Product,
@@ -18,7 +18,6 @@ import {
createCollections,
createTypes,
} from "../../__fixtures__/product"
import { createProductCategories } from "../../__fixtures__/product-category"
jest.setTimeout(300000)
@@ -101,10 +100,10 @@ moduleIntegrationTestRunner<IProductModuleService>({
productTypeOne = types[0]
productTypeTwo = types[1]
const categories = await createProductCategories(
testManager,
productCategoriesData
)
const categories: ProductCategoryDTO[] = []
for (const entry of productCategoriesData) {
categories.push(await service.createCategories(entry))
}
productCategoryOne = categories[0]
productCategoryTwo = categories[1]

View File

@@ -12,9 +12,8 @@ import { Modules } from "@medusajs/modules-sdk"
import { IProductModuleService, ProductDTO } from "@medusajs/types"
import { kebabCase, ProductStatus } from "@medusajs/utils"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { ProductService } from "@services"
import { ProductService, ProductCategoryService } from "@services"
import { moduleIntegrationTestRunner } from "medusa-test-utils"
import { createProductCategories } from "../__fixtures__/product-category"
import {
categoriesData,
productsData,
@@ -25,15 +24,18 @@ jest.setTimeout(30000)
type Service = IProductModuleService & {
productService_: ProductService
productCategoryService_: ProductCategoryService
}
moduleIntegrationTestRunner<Service>({
moduleName: Modules.PRODUCT,
testSuite: ({ MikroOrmWrapper, service: moduleService }) => {
let service: ProductService
let categoryService: ProductCategoryService
beforeEach(() => {
service = moduleService.productService_
categoryService = moduleService.productCategoryService_
})
describe("Product Service", () => {
@@ -351,10 +353,11 @@ moduleIntegrationTestRunner<Service>({
products = await createProductAndTags(testManager, productsData)
workingProduct = products.find((p) => p.id === "test-1") as Product
categories = await createProductCategories(
testManager,
categoriesData
)
categories = []
for (const entry of categoriesData) {
categories.push((await categoryService.create([entry]))[0])
}
workingCategory = (await testManager.findOne(
ProductCategory,
"category-1"
@@ -400,21 +403,21 @@ moduleIntegrationTestRunner<Service>({
id: "category-0",
name: "category 0",
handle: "category-0",
mpath: "category-0.",
mpath: "category-0",
parent_category_id: null,
},
{
id: "category-1",
name: "category 1",
handle: "category-1",
mpath: "category-0.category-1.",
mpath: "category-0.category-1",
parent_category_id: null,
},
{
id: "category-1-a",
name: "category 1 a",
handle: "category-1-a",
mpath: "category-0.category-1.category-1-a.",
mpath: "category-0.category-1.category-1-a",
parent_category_id: null,
},
])

View File

@@ -73,7 +73,7 @@ class ProductCategory {
nullable: false,
default: 0,
})
rank?: number
rank: number
@ManyToOne(() => ProductCategory, {
columnType: "text",
@@ -130,22 +130,7 @@ class ProductCategory {
this.handle = kebabCase(this.name)
}
const { em } = args
let parentCategory: ProductCategory | null = null
if (this.parent_category_id) {
parentCategory = await em.findOne(
ProductCategory,
this.parent_category_id
)
}
if (parentCategory) {
this.mpath = `${parentCategory?.mpath}${this.id}.`
} else {
this.mpath = `${this.id}.`
}
this.mpath = `${this.mpath ? this.mpath + "." : ""}${this.id}`
}
}

View File

@@ -12,20 +12,7 @@ import {
} from "@mikro-orm/core"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { ProductCategory } from "@models"
export type ReorderConditions = {
targetCategoryId: string
originalParentId: string | null
targetParentId: string | null | undefined
originalRank: number
targetRank: number | undefined
shouldChangeParent: boolean
shouldChangeRank: boolean
shouldIncrementRank: boolean
shouldDeleteElement: boolean
}
export const tempReorderRank = 99999
import { UpdateCategoryInput } from "@types"
// eslint-disable-next-line max-len
export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeRepository<ProductCategory> {
@@ -144,10 +131,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
if (include.ancestors) {
let parent = ""
cat.mpath?.split(".").forEach((mpath) => {
if (mpath === "") {
return
}
parentMpaths.add(parent + mpath + ".")
parentMpaths.add(parent + mpath)
parent += mpath + "."
})
}
@@ -257,249 +241,280 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
]
}
async delete(id: string, context: Context = {}): Promise<void> {
async delete(ids: string[], context: Context = {}): Promise<void> {
const manager = super.getActiveManager<SqlEntityManager>(context)
const productCategory = await manager.findOneOrFail(
ProductCategory,
{ id },
{
populate: ["category_children"],
}
await this.baseDelete(ids, context)
await manager.nativeDelete(ProductCategory, { id: ids }, {})
}
async softDelete(
ids: string[],
context: Context = {}
): Promise<[ProductCategory[], Record<string, unknown[]>]> {
const manager = super.getActiveManager<SqlEntityManager>(context)
await this.baseDelete(ids, context)
const categories = await Promise.all(
ids.map(async (id) => {
const productCategory = await manager.findOneOrFail(ProductCategory, {
id,
})
manager.assign(productCategory, { deleted_at: new Date() })
return productCategory
})
)
if (productCategory.category_children.length > 0) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Deleting ProductCategory (${id}) with category children is not allowed`
)
}
manager.persist(categories)
return [categories, {}]
}
const conditions = this.fetchReorderConditions(
productCategory,
{
parent_category_id: productCategory.parent_category_id,
rank: productCategory.rank,
},
true
async restore(
ids: string[],
context: Context = {}
): Promise<[ProductCategory[], Record<string, unknown[]>]> {
const manager = super.getActiveManager<SqlEntityManager>(context)
const categories = await Promise.all(
ids.map(async (id) => {
const productCategory = await manager.findOneOrFail(ProductCategory, {
id,
})
manager.assign(productCategory, { deleted_at: null })
return productCategory
})
)
await this.performReordering(manager, conditions)
await manager.nativeDelete(ProductCategory, { id: id }, {})
manager.persist(categories)
return [categories, {}]
}
async baseDelete(ids: string[], context: Context = {}): Promise<void> {
const manager = super.getActiveManager<SqlEntityManager>(context)
await Promise.all(
ids.map(async (id) => {
const productCategory = await manager.findOneOrFail(
ProductCategory,
{ id },
{
populate: ["category_children"],
}
)
if (productCategory.category_children.length > 0) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Deleting ProductCategory (${id}) with category children is not allowed`
)
}
await this.rerankSiblingsAfterDeletion(manager, productCategory)
})
)
}
async create(
data: ProductTypes.CreateProductCategoryDTO,
data: ProductTypes.CreateProductCategoryDTO[],
context: Context = {}
): Promise<ProductCategory> {
const categoryData = { ...data }
): Promise<ProductCategory[]> {
const manager = super.getActiveManager<SqlEntityManager>(context)
const siblings = await manager.find(ProductCategory, {
parent_category_id: categoryData?.parent_category_id || null,
})
if (!isDefined(categoryData.rank)) {
categoryData.rank = siblings.length
}
const categories = await Promise.all(
data.map(async (entry, i) => {
const categoryData: Partial<ProductCategory> = { ...entry }
const siblingsCount = await manager.count(ProductCategory, {
parent_category_id: categoryData?.parent_category_id || null,
})
const productCategory = manager.create(ProductCategory, categoryData)
if (!isDefined(categoryData.rank)) {
categoryData.rank = siblingsCount + i
} else {
if (categoryData.rank > siblingsCount + i) {
categoryData.rank = siblingsCount + i
}
manager.persist(productCategory)
await this.rerankSiblingsAfterCreation(manager, categoryData)
}
return productCategory
// Set the base mpath if the category has a parent. The model `create` hook will append the own id to the base mpath.
const parentCategoryId =
categoryData.parent_category_id ?? categoryData.parent_category?.id
if (parentCategoryId) {
const parentCategory = await manager.findOne(
ProductCategory,
parentCategoryId
)
if (!parentCategory) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
`Parent category with id: '${parentCategoryId}' does not exist`
)
}
categoryData.mpath = parentCategory.mpath
}
return manager.create(ProductCategory, categoryData as ProductCategory)
})
)
manager.persist(categories)
return categories
}
async update(
id: string,
data: ProductTypes.UpdateProductCategoryDTO,
data: UpdateCategoryInput[],
context: Context = {}
): Promise<ProductCategory> {
const categoryData = { ...data }
): Promise<ProductCategory[]> {
const manager = super.getActiveManager<SqlEntityManager>(context)
const productCategory = await manager.findOneOrFail(ProductCategory, { id })
const categories = await Promise.all(
data.map(async (entry, i) => {
const categoryData: Partial<ProductCategory> = { ...entry }
const productCategory = await manager.findOneOrFail(ProductCategory, {
id: categoryData.id,
})
const conditions = this.fetchReorderConditions(
productCategory,
categoryData
if (
categoryData.parent_category_id &&
categoryData.parent_category_id !== productCategory.parent_category_id
) {
const newParentCategory = await manager.findOne(
ProductCategory,
categoryData.parent_category_id
)
if (!newParentCategory) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
`Parent category with id: '${categoryData.parent_category_id}' does not exist`
)
}
categoryData.mpath = `${newParentCategory.mpath}.${productCategory.id}`
const siblingsCount = await manager.count(ProductCategory, {
parent_category_id: categoryData.parent_category_id,
})
if (!isDefined(categoryData.rank)) {
categoryData.rank = siblingsCount + i
} else {
if (categoryData.rank > siblingsCount + i) {
categoryData.rank = siblingsCount + i
}
await this.rerankSiblingsAfterCreation(manager, categoryData)
}
await this.rerankSiblingsAfterDeletion(manager, productCategory)
}
// In the case of the parent being updated, we do a delete/create reranking. If only the rank was updated, we need to shift all siblings
else if (isDefined(categoryData.rank)) {
const siblingsCount = await manager.count(ProductCategory, {
parent_category_id: productCategory.parent_category_id,
})
// We don't cout the updated category itself.
if (categoryData.rank > siblingsCount - 1 + i) {
categoryData.rank = siblingsCount - 1 + i
}
await this.rerankAllSiblings(
manager,
productCategory,
categoryData as ProductCategory
)
}
for (const key in categoryData) {
if (isDefined(categoryData[key])) {
productCategory[key] = categoryData[key]
}
}
manager.assign(productCategory, categoryData)
return productCategory
})
)
if (conditions.shouldChangeRank || conditions.shouldChangeParent) {
categoryData.rank = tempReorderRank
}
// await this.transformParentIdToEntity(categoryData)
for (const key in categoryData) {
if (isDefined(categoryData[key])) {
productCategory[key] = categoryData[key]
}
}
manager.assign(productCategory, categoryData)
manager.persist(productCategory)
await this.performReordering(manager, conditions)
return productCategory
manager.persist(categories)
return categories
}
protected fetchReorderConditions(
productCategory: ProductCategory,
data: ProductTypes.UpdateProductCategoryDTO,
shouldDeleteElement = false
): ReorderConditions {
const originalParentId = productCategory.parent_category_id || null
const targetParentId = data.parent_category_id
const originalRank = productCategory.rank || 0
const targetRank = data.rank
const shouldChangeParent =
targetParentId !== undefined && targetParentId !== originalParentId
const shouldChangeRank =
shouldChangeParent ||
(isDefined(targetRank) && originalRank !== targetRank)
return {
targetCategoryId: productCategory.id,
originalParentId,
targetParentId,
originalRank,
targetRank,
shouldChangeParent,
shouldChangeRank,
shouldIncrementRank: false,
shouldDeleteElement,
}
}
protected async performReordering(
protected async rerankSiblingsAfterDeletion(
manager: SqlEntityManager,
conditions: ReorderConditions
): Promise<void> {
const { shouldChangeParent, shouldChangeRank, shouldDeleteElement } =
conditions
removedSibling: Partial<ProductCategory>
) {
const affectedSiblings = await manager.find(ProductCategory, {
parent_category_id: removedSibling.parent_category_id,
rank: { $gt: removedSibling.rank },
})
if (!(shouldChangeParent || shouldChangeRank || shouldDeleteElement)) {
const updatedSiblings = affectedSiblings.map((sibling) => {
manager.assign(sibling, { rank: sibling.rank - 1 })
return sibling
})
manager.persist(updatedSiblings)
}
protected async rerankSiblingsAfterCreation(
manager: SqlEntityManager,
addedSibling: Partial<ProductCategory>
) {
const affectedSiblings = await manager.find(ProductCategory, {
parent_category_id: addedSibling.parent_category_id,
rank: { $gte: addedSibling.rank },
})
const updatedSiblings = affectedSiblings.map((sibling) => {
manager.assign(sibling, { rank: sibling.rank + 1 })
return sibling
})
manager.persist(updatedSiblings)
}
protected async rerankAllSiblings(
manager: SqlEntityManager,
originalSibling: Partial<ProductCategory> & { rank: number },
updatedSibling: Partial<ProductCategory> & { rank: number }
) {
if (originalSibling.rank === updatedSibling.rank) {
return
}
// If we change parent, we need to shift the siblings to eliminate the
// rank occupied by the targetCategory in the original parent.
shouldChangeParent &&
(await this.shiftSiblings(manager, {
...conditions,
targetRank: conditions.originalRank,
targetParentId: conditions.originalParentId,
}))
if (originalSibling.rank < updatedSibling.rank) {
const siblings = await manager.find(
ProductCategory,
{
parent_category_id: originalSibling.parent_category_id,
rank: { $gt: originalSibling.rank, $lte: updatedSibling.rank },
},
{ orderBy: { rank: "ASC" } }
)
// If we change parent, we need to shift the siblings of the new parent
// to create a rank that the targetCategory will occupy.
shouldChangeParent &&
shouldChangeRank &&
(await this.shiftSiblings(manager, {
...conditions,
shouldIncrementRank: true,
}))
const updatedSiblings = siblings.map((sibling) => {
manager.assign(sibling, { rank: sibling.rank - 1 })
return sibling
})
// If we only change rank, we need to shift the siblings
// to create a rank that the targetCategory will occupy.
;((!shouldChangeParent && shouldChangeRank) || shouldDeleteElement) &&
(await this.shiftSiblings(manager, {
...conditions,
targetParentId: conditions.originalParentId,
}))
}
protected async shiftSiblings(
manager: SqlEntityManager,
conditions: ReorderConditions
): Promise<void> {
let { shouldIncrementRank, targetRank } = conditions
const {
shouldChangeParent,
originalRank,
targetParentId,
targetCategoryId,
shouldDeleteElement,
} = conditions
// The current sibling count will replace targetRank if
// targetRank is greater than the count of siblings.
const siblingCount = await manager.count(ProductCategory, {
parent_category_id: targetParentId || null,
id: { $ne: targetCategoryId },
})
// The category record that will be placed at the requested rank
// We've temporarily placed it at a temporary rank that is
// beyond a reasonable value (tempReorderRank)
const targetCategory = await manager.findOne(ProductCategory, {
id: targetCategoryId,
parent_category_id: targetParentId || null,
rank: tempReorderRank,
})
// If the targetRank is not present, or if targetRank is beyond the
// rank of the last category, we set the rank as the last rank
if (targetRank === undefined || targetRank > siblingCount) {
targetRank = siblingCount
}
let rankCondition
// If parent doesn't change, we only need to get the ranks
// in between the original rank and the target rank.
if (shouldChangeParent || shouldDeleteElement) {
rankCondition = { $gte: targetRank }
} else if (originalRank > targetRank) {
shouldIncrementRank = true
rankCondition = { $gte: targetRank, $lte: originalRank }
manager.persist(updatedSiblings)
} else {
shouldIncrementRank = false
rankCondition = { $gte: originalRank, $lte: targetRank }
const siblings = await manager.find(
ProductCategory,
{
parent_category_id: originalSibling.parent_category_id,
rank: { $gte: updatedSibling.rank, $lt: originalSibling.rank },
},
{ orderBy: { rank: "ASC" } }
)
const updatedSiblings = siblings.map((sibling) => {
manager.assign(sibling, { rank: sibling.rank + 1 })
return sibling
})
manager.persist(updatedSiblings)
}
// Scope out the list of siblings that we need to shift up or down
const siblingsToShift = await manager.find(
ProductCategory,
{
parent_category_id: targetParentId || null,
rank: rankCondition,
id: { $ne: targetCategoryId },
},
{
orderBy: { rank: shouldIncrementRank ? "DESC" : "ASC" },
}
)
// Depending on the conditions, we get a subset of the siblings
// and independently shift them up or down a rank
for (let index = 0; index < siblingsToShift.length; index++) {
const sibling = siblingsToShift[index]
// Depending on the condition, we could also have the targetCategory
// in the siblings list, we skip shifting the target until all other siblings
// have been shifted.
if (sibling.id === targetCategoryId) {
continue
}
if (!isDefined(sibling.rank)) {
throw new Error("sibling rank is not defined")
}
const rank = shouldIncrementRank ? ++sibling.rank! : --sibling.rank!
manager.assign(sibling, { rank })
manager.persist(sibling)
}
// The targetCategory will not be present in the query when we are shifting
// siblings of the old parent of the targetCategory.
if (!targetCategory) {
return
}
// Place the targetCategory in the requested rank
manager.assign(targetCategory, { rank: targetRank })
manager.persist(targetCategory)
}
}

View File

@@ -10,11 +10,11 @@ import {
} from "@medusajs/utils"
import { ProductCategory } from "@models"
import { ProductCategoryRepository } from "@repositories"
import { UpdateCategoryInput } from "@types"
type InjectedDependencies = {
productCategoryRepository: DAL.TreeRepositoryService
}
export default class ProductCategoryService<
TEntity extends ProductCategory = ProductCategory
> {
@@ -24,6 +24,7 @@ export default class ProductCategoryService<
this.productCategoryRepository_ = productCategoryRepository
}
// TODO: Add support for object filter
@InjectManager("productCategoryRepository_")
async retrieve(
productCategoryId: string,
@@ -44,6 +45,8 @@ export default class ProductCategoryService<
config
)
// TODO: Currently remoteQuery doesn't allow passing custom objects, so the `include*` are part of the filters
// Modify remoteQuery to allow passing custom objects
const transformOptions = {
includeDescendantsTree: true,
}
@@ -140,30 +143,47 @@ export default class ProductCategoryService<
@InjectTransactionManager("productCategoryRepository_")
async create(
data: ProductTypes.CreateProductCategoryDTO,
data: ProductTypes.CreateProductCategoryDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
): Promise<TEntity[]> {
return (await (
this.productCategoryRepository_ as unknown as ProductCategoryRepository
).create(data, sharedContext)) as TEntity
).create(data, sharedContext)) as TEntity[]
}
@InjectTransactionManager("productCategoryRepository_")
async update(
id: string,
data: ProductTypes.UpdateProductCategoryDTO,
data: UpdateCategoryInput[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
): Promise<TEntity[]> {
return (await (
this.productCategoryRepository_ as unknown as ProductCategoryRepository
).update(id, data, sharedContext)) as TEntity
).update(data, sharedContext)) as TEntity[]
}
@InjectTransactionManager("productCategoryRepository_")
async delete(
id: string,
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.productCategoryRepository_.delete(id, sharedContext)
await this.productCategoryRepository_.delete(ids, sharedContext)
}
async softDelete(
ids: string[],
@MedusaContext() sharedContext?: Context
): Promise<Record<string, string[]> | void> {
return (await (
this.productCategoryRepository_ as unknown as ProductCategoryRepository
).softDelete(ids, sharedContext)) as any
}
async restore(
ids: string[],
@MedusaContext() sharedContext?: Context
): Promise<Record<string, string[]> | void> {
return (await (
this.productCategoryRepository_ as unknown as ProductCategoryRepository
).restore(ids, sharedContext)) as any
}
}

View File

@@ -44,6 +44,7 @@ import {
ProductCollectionEvents,
ProductEventData,
ProductEvents,
UpdateCategoryInput,
UpdateCollectionInput,
UpdateProductInput,
UpdateProductOptionInput,
@@ -1114,67 +1115,166 @@ export default class ProductModuleService<
return collections
}
createCategories(
data: ProductTypes.CreateProductCategoryDTO[],
sharedContext?: Context
): Promise<ProductTypes.ProductCategoryDTO[]>
createCategories(
data: ProductTypes.CreateProductCategoryDTO,
sharedContext?: Context
): Promise<ProductTypes.ProductCategoryDTO>
@InjectManager("baseRepository_")
async createCategory(
data: ProductTypes.CreateProductCategoryDTO,
@EmitEvents()
async createCategories(
data:
| ProductTypes.CreateProductCategoryDTO[]
| ProductTypes.CreateProductCategoryDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductCategoryDTO> {
const result = await this.createCategory_(data, sharedContext)
): Promise<
ProductTypes.ProductCategoryDTO[] | ProductTypes.ProductCategoryDTO
> {
const input = Array.isArray(data) ? data : [data]
return await this.baseRepository_.serialize(result)
}
@InjectTransactionManager("baseRepository_")
async createCategory_(
data: ProductTypes.CreateProductCategoryDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<ProductCategory> {
const productCategory = await this.productCategoryService_.create(
data,
const categories = await this.productCategoryService_.create(
input,
sharedContext
)
await this.eventBusModuleService_?.emit<ProductCategoryEventData>({
eventName: ProductCategoryEvents.CATEGORY_CREATED,
data: { id: productCategory.id },
const createdCategories = await this.baseRepository_.serialize<
ProductTypes.ProductCategoryDTO[]
>(categories)
eventBuilders.createdProductCategory({
data: createdCategories,
sharedContext,
})
return productCategory
return Array.isArray(data) ? createdCategories : createdCategories[0]
}
async upsertCategories(
data: ProductTypes.UpsertProductCategoryDTO[],
sharedContext?: Context
): Promise<ProductTypes.ProductCategoryDTO[]>
async upsertCategories(
data: ProductTypes.UpsertProductCategoryDTO,
sharedContext?: Context
): Promise<ProductTypes.ProductCategoryDTO>
@InjectTransactionManager("baseRepository_")
async updateCategory(
categoryId: string,
@EmitEvents()
async upsertCategories(
data:
| ProductTypes.UpsertProductCategoryDTO[]
| ProductTypes.UpsertProductCategoryDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
ProductTypes.ProductCategoryDTO[] | ProductTypes.ProductCategoryDTO
> {
const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter(
(category): category is UpdateCategoryInput => !!category.id
)
const forCreate = input.filter(
(category): category is ProductTypes.CreateProductCategoryDTO =>
!category.id
)
let created: ProductCategory[] = []
let updated: ProductCategory[] = []
if (forCreate.length) {
created = await this.productCategoryService_.create(
forCreate,
sharedContext
)
}
if (forUpdate.length) {
updated = await this.productCategoryService_.update(
forUpdate,
sharedContext
)
}
const createdCategories = await this.baseRepository_.serialize<
ProductTypes.ProductCategoryDTO[]
>(created)
const updatedCategories = await this.baseRepository_.serialize<
ProductTypes.ProductCategoryDTO[]
>(updated)
eventBuilders.createdProductCategory({
data: createdCategories,
sharedContext,
})
eventBuilders.updatedProductCategory({
data: updatedCategories,
sharedContext,
})
const result = [...createdCategories, ...updatedCategories]
return Array.isArray(data) ? result : result[0]
}
updateCategories(
id: string,
data: ProductTypes.UpdateProductCategoryDTO,
sharedContext?: Context
): Promise<ProductTypes.ProductCategoryDTO>
updateCategories(
selector: ProductTypes.FilterableProductTypeProps,
data: ProductTypes.UpdateProductCategoryDTO,
sharedContext?: Context
): Promise<ProductTypes.ProductCategoryDTO[]>
@InjectManager("baseRepository_")
@EmitEvents()
async updateCategories(
idOrSelector: string | ProductTypes.FilterableProductTypeProps,
data: ProductTypes.UpdateProductCategoryDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductCategoryDTO> {
const productCategory = await this.productCategoryService_.update(
categoryId,
data,
): Promise<
ProductTypes.ProductCategoryDTO[] | ProductTypes.ProductCategoryDTO
> {
let normalizedInput: UpdateCategoryInput[] = []
if (isString(idOrSelector)) {
// Check if the type exists in the first place
await this.productCategoryService_.retrieve(
idOrSelector,
{},
sharedContext
)
normalizedInput = [{ id: idOrSelector, ...data }]
} else {
const categories = await this.productCategoryService_.list(
idOrSelector,
{},
sharedContext
)
normalizedInput = categories.map((type) => ({
id: type.id,
...data,
}))
}
const categories = await this.productCategoryService_.update(
normalizedInput,
sharedContext
)
await this.eventBusModuleService_?.emit<ProductCategoryEventData>({
eventName: ProductCategoryEvents.CATEGORY_UPDATED,
data: { id: productCategory.id },
const updatedCategories = await this.baseRepository_.serialize<
ProductTypes.ProductCategoryDTO[]
>(categories)
eventBuilders.updatedProductCategory({
data: updatedCategories,
sharedContext,
})
return await this.baseRepository_.serialize(productCategory, {
populate: true,
})
}
@InjectTransactionManager("baseRepository_")
async deleteCategory(
categoryId: string,
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.productCategoryService_.delete(categoryId, sharedContext)
await this.eventBusModuleService_?.emit<ProductCategoryEventData>({
eventName: ProductCategoryEvents.CATEGORY_DELETED,
data: { id: categoryId },
})
return isString(idOrSelector) ? updatedCategories[0] : updatedCategories
}
create(

View File

@@ -59,6 +59,10 @@ export type UpdateTypeInput = ProductTypes.UpdateProductTypeDTO & {
id: string
}
export type UpdateCategoryInput = ProductTypes.UpdateProductCategoryDTO & {
id: string
}
export type UpdateTagInput = ProductTypes.UpdateProductTagDTO & {
id: string
}

View File

@@ -99,4 +99,22 @@ export const eventBuilders = {
object: "product_tag",
eventsEnum: ProductEvents,
}),
createdProductCategory: eventBuilderFactory({
source: Modules.PRODUCT,
action: CommonEvents.CREATED,
object: "product_category",
eventsEnum: ProductEvents,
}),
updatedProductCategory: eventBuilderFactory({
source: Modules.PRODUCT,
action: CommonEvents.UPDATED,
object: "product_category",
eventsEnum: ProductEvents,
}),
deletedProductCategory: eventBuilderFactory({
source: Modules.PRODUCT,
action: CommonEvents.DELETED,
object: "product_category",
eventsEnum: ProductEvents,
}),
}