Skip to content

Commit

Permalink
feat(medusa): Add ProductCategory model (medusajs#2945)
Browse files Browse the repository at this point in the history
  • Loading branch information
riqwan authored Jan 5, 2023
1 parent a153289 commit 3f44abe
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/honest-guests-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---

feat(nested-categories): Introduces a model and migration to create category table that can be nested
Original file line number Diff line number Diff line change
@@ -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",
}),
])
})
})
})
38 changes: 38 additions & 0 deletions packages/medusa/src/migrations/1672906846559-product-category.ts
Original file line number Diff line number Diff line change
@@ -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"`)
}
}
1 change: 1 addition & 0 deletions packages/medusa/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
112 changes: 112 additions & 0 deletions packages/medusa/src/models/product-category.ts
Original file line number Diff line number Diff line change
@@ -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
*/

0 comments on commit 3f44abe

Please sign in to comment.