feat(medusa): Add ProductCategory model (#2945)

This commit is contained in:
Riqwan Thamir
2023-01-05 19:10:46 +01:00
committed by GitHub
parent a153289ba3
commit 3f44abe01a
5 changed files with 232 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
feat(nested-categories): Introduces a model and migration to create category table that can be nested

View File

@@ -0,0 +1,76 @@
import path from "path"
import { ProductCategory } from "@medusajs/medusa"
import { initDb, useDb } from "../../../helpers/use-db"
describe("Product Categories", () => {
let dbConnection
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."))
dbConnection = await initDb({ cwd })
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
describe("Tree Queries (Materialized Paths)", () => {
it("can fetch ancestors, descendents and root product categories", async () => {
const productCategoryRepository = dbConnection.getTreeRepository(ProductCategory)
const a1 = productCategoryRepository.create({ name: 'a1', handle: 'a1' })
await productCategoryRepository.save(a1)
const a11 = productCategoryRepository.create({ name: 'a11', handle: 'a11', parent_category: a1 })
await productCategoryRepository.save(a11)
const a111 = productCategoryRepository.create({ name: 'a111', handle: 'a111', parent_category: a11 })
await productCategoryRepository.save(a111)
const a12 = productCategoryRepository.create({ name: 'a12', handle: 'a12', parent_category: a1 })
await productCategoryRepository.save(a12)
const rootCategories = await productCategoryRepository.findRoots()
expect(rootCategories).toEqual([
expect.objectContaining({
name: "a1",
})
])
const a11Parent = await productCategoryRepository.findAncestors(a11)
expect(a11Parent).toEqual([
expect.objectContaining({
name: "a1",
}),
expect.objectContaining({
name: "a11",
}),
])
const a1Children = await productCategoryRepository.findDescendants(a1)
expect(a1Children).toEqual([
expect.objectContaining({
name: "a1",
}),
expect.objectContaining({
name: "a11",
}),
expect.objectContaining({
name: "a111",
}),
expect.objectContaining({
name: "a12",
}),
])
})
})
})

View File

@@ -0,0 +1,38 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class productCategory1672906846559 implements MigrationInterface {
name = "productCategory1672906846559"
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE "product_category"
(
"id" character varying NOT NULL,
"name" text NOT NULL,
"handle" text NOT NULL,
"parent_category_id" character varying,
"mpath" text,
"is_active" boolean DEFAULT false,
"is_internal" boolean DEFAULT false,
"deleted_at" TIMESTAMP WITH TIME ZONE,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "PK_qgguwbn1cwstxk93efl0px9oqwt" PRIMARY KEY ("id")
)
`)
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_product_category_handle" ON "product_category" ("handle") WHERE deleted_at IS NULL`
)
await queryRunner.query(
`CREATE INDEX "IDX_product_category_path" ON "product_category" ("mpath")`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_product_category_path"`)
await queryRunner.query(`DROP INDEX "IDX_product_category_handle"`)
await queryRunner.query(`DROP TABLE "product_category"`)
}
}

View File

@@ -2,6 +2,7 @@ export * from "./address"
export * from "./analytics-config"
export * from "./batch-job"
export * from "./cart"
export * from "./product-category"
export * from "./claim-image"
export * from "./claim-item"
export * from "./claim-order"

View File

@@ -0,0 +1,112 @@
import { generateEntityId } from "../utils/generate-entity-id"
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
import {
BeforeInsert,
Index,
Entity,
Tree,
Column,
PrimaryGeneratedColumn,
TreeChildren,
TreeParent,
TreeLevelColumn,
JoinColumn,
} from "typeorm"
@Entity()
@Tree("materialized-path")
export class ProductCategory extends SoftDeletableEntity {
@Column()
name: string
@Index({ unique: true, where: "deleted_at IS NULL" })
@Column({ nullable: false })
handle: string
@Column()
is_active: Boolean
@Column()
is_internal: Boolean
// The materialized path column is added dynamically by typeorm. Commenting this here for it
// to not be a mystery
// https://github.com/typeorm/typeorm/blob/62518ae1226f22b2f230afa615532c92f1544f01/src/metadata-builder/EntityMetadataBuilder.ts#L615
// @Column({ nullable: true, default: '' })
// mpath: String
@TreeParent()
@JoinColumn({ name: 'parent_category_id' })
parent_category: ProductCategory | null
// Typeorm also keeps track of the category's parent at all times.
// TODO: Uncomment this if there is a usecase for accessing this.
// @Column()
// parent_category_id: ProductCategory
@TreeChildren({ cascade: true })
category_children: ProductCategory[]
@BeforeInsert()
private beforeInsert(): void {
this.id = generateEntityId(this.id, "pcat")
}
}
/**
* @schema productCategory
* title: "ProductCategory"
* description: "Represents a product category"
* x-resourceId: productCategory
* type: object
* required:
* - name
* - handle
* properties:
* id:
* type: string
* description: The product category's ID
* example: pcat_01G2SG30J8C85S4A5CHM2S1NS2
* name:
* type: string
* description: The product category's name
* example: Regular Fit
* handle:
* description: "A unique string that identifies the Category - example: slug structures."
* type: string
* example: regular-fit
* path:
* type: string
* description: A string for Materialized Paths - used for finding ancestors and descendents
* example: pcat_id1.pcat_id2.pcat_id3
* is_internal:
* type: boolean
* description: A flag to make product category an internal category for admins
* default: false
* is_active:
* type: boolean
* description: A flag to make product category visible/hidden in the store front
* default: false
* category_children:
* description: Available if the relation `category_children` are expanded.
* type: array
* items:
* type: object
* description: A product category object.
* parent_category:
* description: Available if the relation `parent_category` is expanded.
* type: object
* description: A product category object.
* created_at:
* type: string
* description: "The date with timezone at which the resource was created."
* format: date-time
* updated_at:
* type: string
* description: "The date with timezone at which the resource was updated."
* format: date-time
* deleted_at:
* type: string
* description: "The date with timezone at which the resource was deleted."
* format: date-time
*/