feat: Product Module (#4161)
* chore: boilerplate setup * wip: add Product, ProductTag, ProductType, ProductCollection models * wip: `IProductService` definition * wip: test function in index, build passing * fix: where condition * chore: get boilerplate working with modules sdk, create a boilerplate test, create product variant model, register services properly * chore: added variant to model * chore: changed definition details + add migrator * cleanup and update product entity * Update product unique index to include soft deleted * Migrations tests * generated migration * update dev orm config * add path aliases * WIP * chore: added boilerplate integration test + database helper + product variant migraiton + model * chore: remove old test utils * update ts and jest config to include path aliases * tweak config * WIP migrations variant * Migrations round * integration tests migrations polishing * chore: fixed issues with test db * fix path aliases when published * use ts-alias * fix connection loader * fixes * wip: product list * (WIP): Data access layer * (WIP): DAL cleanup services * wip: `ProductTag` DAL * wip: `ProductTag` expose list in product service * (WIP): Continue DAL and test list product filtering/populate * WIP: unit tests * chore: added tests for service - productvariant * chore: WIP finding issues with orm manager fork * WIP fix fields selection * chore: make text fixes work * (WIP) product integration * (WIP) product integration * chore: create a product in variant test * list product with relations * wip: `ProductTag` service + integrations * chore: added with and without serialization example * chore: remove only in spec * wip: `ProductCollection` service + integrations * uncomment product.variants * Update type IProductService * (WIP) type work * (WIP) Product variants relation * WIP: replacable data layer * (WIP): Use bundle types * WIP: update type * WIP upadte tests * WIP * wip: options/option values entites * (WIP): cleanup * wip: add option value to variant, fix collection * Integration tests for custom data access layer * update tests * chore: merge with latest branch * chore: scope tests to relations and add category to models/index * chore: ignore dist folders for jest * chore: modularize spec data file * improve DX * module fixture naming * chore: added category tests + fix model * chore: use kebab case * chore: allow scoping products by category id * chore: replace `kebabCase` import * feat: add `deleted_at` to options * improve typings * chore: wip * fix query util * revert webpack * fix: update option models, create option DTOs, tests wip, fix `deduplicateIfNecessary` returning `undefined` * fix: merge conflict * WIP connection * rm unsues deps * fix migrations * fix query util * chore: adds mpath on creation * WIP update types * improve typings * WIP typeings improvement * WIP * deps * chore: package medusa/product ot medusa-commerce/product * chore: added product categories service + descendants filter * add missing index * Add support for strict categories not in * Add support for strict categories not in * lint * rename module * rename module * Create small-ducks-doubt.md * yarn lock * update initialise * chore: fix/finalise DTOs * fix: wrong types in `IProductService` * fix type * Load database config from env if present (#4175) * Load database config from env if present * Load database config from env if present * options optionnal * update util * add defaults * improve filterable interfaces * fix import * fix types * remove medusa-telemetry from modules-sdk * WIP fixing webpack issues when bootstraping module * cleanup * improve loading driver options * cleanup * yarn lock * fix import * improve sdk types and naming * align orther modules initialise method * fix module tests with singleton module * fix module tests with singleton module * add up/down migration scripts * update types * scripts * cleanup migrations and scripts * hash module singleton * cleanup migration * cleanup * fix stringifyCircular usage * improvements * fix deps * fix deps * improve load config utils * improve load config utils * fix deps * add declaration to the build * update yarn * Do not resolve a module path if the exports are explicitly given * fix module registration resolution path when exports are provided. Explicitly check for false and assign an empty string in this scenario for segregation purpose * add comment * fix migration options to prevent set replica errors * chore: update types to a proper depedency * chore: update type package * add seed scripts * Add descriptive error during database config loading * use MedusaError * chore: added lodash to package * add more test to the database config loader util * create bin scripts * add bin * update argv retrieval * update package.json * chore: add product category to injected deps * chore: replace with product category service * move dotenv usage to the functions * do not load db if there is custom manager * chore: fix some tests on products repo * chore: fixed product spec * chore: skip products module on modules register * stringifyCircular update * chore: fix incorrect module resolution * fix: circular stringify and non required module loading * yarn lock * target es5 * chore: mikro-orm back to 5.7.4 * revert module registry * skip external modules * es2020 * update indexes, migration and integration tests * rm only * unit test script should only run unit tests * Exclude product integration from the unit tests and make use of the global integration script to run all packages integration tests * fix integration tests * improve setup * cleanup * log error on setup fail * Create enum like for package names * chore: remove EOL * chore: review part 2 * renamve gateway to productModuleService * chore: added filters and collections to productmoduleservice * chore: add collection to the singleton instance * chore: remove skipped test + add todo * fix indexes on fields and relations + update migration * update yarn lock * update idx * add foreign key * rename interface and add listCategories * rename product module definition --------- Co-authored-by: fPolic <frane@medusajs.com> Co-authored-by: Riqwan Thamir <rmthamir@gmail.com> Co-authored-by: Carlos R. L. Rodrigues <rodrigolr@gmail.com>
This commit is contained in:
co-authored by
fPolic
Riqwan Thamir
Carlos R. L. Rodrigues
parent
ffeeb84d97
commit
14c0f62f84
@@ -0,0 +1,6 @@
|
||||
export { default as Product } from "./product"
|
||||
export { default as ProductCategory } from "./product-category"
|
||||
export { default as ProductCollection } from "./product-collection"
|
||||
export { default as ProductTag } from "./product-tag"
|
||||
export { default as ProductType } from "./product-type"
|
||||
export { default as ProductVariant } from "./product-variant"
|
||||
@@ -0,0 +1,101 @@
|
||||
import { generateEntityId, kebabCase } from "@medusajs/utils"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
EventArgs,
|
||||
Index,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
Unique,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import Product from "./product"
|
||||
|
||||
@Entity({ tableName: "product_category" })
|
||||
class ProductCategory {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text", nullable: false })
|
||||
name: string
|
||||
|
||||
@Property({ columnType: "text", default: "", nullable: false })
|
||||
description?: string
|
||||
|
||||
@Unique({
|
||||
name: "IDX_product_category_handle",
|
||||
properties: ["handle"],
|
||||
})
|
||||
@Property({ columnType: "text", nullable: false })
|
||||
handle?: string
|
||||
|
||||
@Index({
|
||||
name: "IDX_product_category_path",
|
||||
properties: ["mpath"],
|
||||
})
|
||||
@Property({ columnType: "text", nullable: false })
|
||||
mpath?: string
|
||||
|
||||
@Property({ columnType: "boolean", default: false })
|
||||
is_active?: boolean
|
||||
|
||||
@Property({ columnType: "boolean", default: false })
|
||||
is_internal?: boolean
|
||||
|
||||
@Property({ columnType: "numeric", nullable: false, default: 0 })
|
||||
rank?: number
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
parent_category_id?: string
|
||||
|
||||
@ManyToOne(() => ProductCategory, { nullable: true })
|
||||
parent_category: ProductCategory
|
||||
|
||||
@OneToMany({
|
||||
entity: () => ProductCategory,
|
||||
mappedBy: (productCategory) => productCategory.parent_category,
|
||||
})
|
||||
category_children = new Collection<ProductCategory>(this)
|
||||
|
||||
@Property({ onCreate: () => new Date(), columnType: "timestamptz" })
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@ManyToMany(() => Product, (product) => product.categories)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@BeforeCreate()
|
||||
async onCreate(args: EventArgs<ProductCategory>) {
|
||||
this.id = generateEntityId(this.id, "pcat")
|
||||
|
||||
if (!this.handle) {
|
||||
this.handle = kebabCase(this.name)
|
||||
}
|
||||
|
||||
const { em } = args
|
||||
const parentCategoryId = args.changeSet?.entity?.parent_category?.id
|
||||
let parentCategory: ProductCategory | null = null
|
||||
|
||||
if (parentCategoryId) {
|
||||
parentCategory = await em.findOne(ProductCategory, parentCategoryId)
|
||||
}
|
||||
|
||||
if (parentCategory) {
|
||||
this.mpath = `${parentCategory?.mpath}${this.id}.`
|
||||
} else {
|
||||
this.mpath = `${this.id}.`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductCategory
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
Unique,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import { generateEntityId, kebabCase } from "@medusajs/utils"
|
||||
|
||||
@Entity({ tableName: "product_collection" })
|
||||
class ProductCollection {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
title: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
@Unique({
|
||||
name: "IDX_product_collection_handle_unique",
|
||||
properties: ["handle"],
|
||||
})
|
||||
handle: string
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "pcol")
|
||||
|
||||
if (!this.handle) {
|
||||
this.handle = kebabCase(this.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductCollection
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
|
||||
import ProductOption from "./product-option"
|
||||
import { ProductVariant } from "./index"
|
||||
|
||||
@Entity({ tableName: "product_option_value" })
|
||||
class ProductOptionValue {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
value: string
|
||||
|
||||
@ManyToOne(() => ProductOption, {
|
||||
index: "IDX_product_option_value_product_option",
|
||||
})
|
||||
option: ProductOption
|
||||
|
||||
@ManyToOne(() => ProductVariant, { onDelete: "cascade" })
|
||||
variant: ProductVariant
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
|
||||
@BeforeCreate()
|
||||
beforeCreate() {
|
||||
this.id = generateEntityId(this.id, "optval")
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductOptionValue
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import { Product } from "./index"
|
||||
import ProductOptionValue from "./product-option-value"
|
||||
|
||||
@Entity({ tableName: "product_option" })
|
||||
class ProductOption {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
title: string
|
||||
|
||||
@Property({ persist: false })
|
||||
product_id!: number
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
index: "IDX_product_option_product_id",
|
||||
})
|
||||
product: Product
|
||||
|
||||
@OneToMany(() => ProductOptionValue, (value) => value.option, {
|
||||
cascade: [Cascade.REMOVE],
|
||||
})
|
||||
values = new Collection<ProductOptionValue>(this)
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
|
||||
@BeforeCreate()
|
||||
beforeCreate() {
|
||||
this.id = generateEntityId(this.id, "opt")
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductOption
|
||||
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
ManyToMany,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
|
||||
@Entity({ tableName: "product_tag" })
|
||||
class ProductTag {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
value: string
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
|
||||
@ManyToMany(() => Product, (product) => product.tags)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "ptag")
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductTag
|
||||
@@ -0,0 +1,25 @@
|
||||
import { BeforeCreate, Entity, PrimaryKey, Property } from "@mikro-orm/core"
|
||||
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
|
||||
@Entity({ tableName: "product_type" })
|
||||
class ProductType {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
value: string
|
||||
|
||||
@Property({ columnType: "json", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "ptyp")
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductType
|
||||
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
Unique,
|
||||
} from "@mikro-orm/core"
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import { Product } from "@models"
|
||||
import ProductOptionValue from "./product-option-value"
|
||||
|
||||
type OptionalFields =
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "updated_at"
|
||||
| "deleted_at"
|
||||
| "allow_backorder"
|
||||
| "manage_inventory"
|
||||
| "product"
|
||||
| "product_id"
|
||||
|
||||
@Entity({ tableName: "product_variant" })
|
||||
class ProductVariant {
|
||||
[OptionalProps]?: OptionalFields
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
title: string
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
@Unique({
|
||||
name: "IDX_product_variant_sku_unique",
|
||||
properties: ["sku"],
|
||||
})
|
||||
sku?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
@Unique({
|
||||
name: "IDX_product_variant_barcode_unique",
|
||||
properties: ["barcode"],
|
||||
})
|
||||
barcode?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
@Unique({
|
||||
name: "IDX_product_variant_ean_unique",
|
||||
properties: ["ean"],
|
||||
})
|
||||
ean?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
@Unique({
|
||||
name: "IDX_product_variant_upc_unique",
|
||||
properties: ["upc"],
|
||||
})
|
||||
upc?: string | null
|
||||
|
||||
// Note: Upon serialization, this turns to a string. This is on purpose, because you would loose
|
||||
// precision if you cast numeric to JS number, as JS number is a float.
|
||||
// Ref: https://github.com/mikro-orm/mikro-orm/issues/2295
|
||||
@Property({ columnType: "numeric" })
|
||||
inventory_quantity: number
|
||||
|
||||
@Property({ columnType: "boolean", default: false })
|
||||
allow_backorder: boolean
|
||||
|
||||
@Property({ columnType: "boolean", default: true })
|
||||
manage_inventory: boolean
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
hs_code?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
origin_country?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
mid_code?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
material?: string | null
|
||||
|
||||
@Property({ columnType: "numeric", nullable: true })
|
||||
weight?: number | null
|
||||
|
||||
@Property({ columnType: "numeric", nullable: true })
|
||||
length?: number | null
|
||||
|
||||
@Property({ columnType: "numeric", nullable: true })
|
||||
height?: number | null
|
||||
|
||||
@Property({ columnType: "numeric", nullable: true })
|
||||
width?: number | null
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({ columnType: "numeric", nullable: true })
|
||||
variant_rank?: number | null
|
||||
|
||||
@Property({ persist: false })
|
||||
product_id!: string
|
||||
|
||||
@Property({ onCreate: () => new Date(), columnType: "timestamptz" })
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
onDelete: "cascade",
|
||||
index: "IDX_product_variant_product_id_index",
|
||||
fieldName: "product_id",
|
||||
})
|
||||
product!: Product
|
||||
|
||||
@OneToMany(() => ProductOptionValue, (optionValue) => optionValue.variant, {
|
||||
cascade: [Cascade.PERSIST, Cascade.REMOVE],
|
||||
})
|
||||
options = new Collection<ProductOptionValue>(this)
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "variant")
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductVariant
|
||||
@@ -0,0 +1,150 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Enum,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
Unique,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
import { generateEntityId, kebabCase } from "@medusajs/utils"
|
||||
import ProductCategory from "./product-category"
|
||||
import ProductCollection from "./product-collection"
|
||||
import ProductOption from "./product-option"
|
||||
import ProductTag from "./product-tag"
|
||||
import ProductType from "./product-type"
|
||||
import ProductVariant from "./product-variant"
|
||||
|
||||
type OptionalRelations = "collection" | "type"
|
||||
type OptionalFields =
|
||||
| "is_giftcard"
|
||||
| "discountable"
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "deleted_at"
|
||||
|
||||
@Entity({ tableName: "product" })
|
||||
class Product {
|
||||
[OptionalProps]?: OptionalRelations | OptionalFields
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
title: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
@Unique({
|
||||
name: "IDX_product_handle_unique",
|
||||
properties: ["handle"],
|
||||
})
|
||||
handle?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
subtitle?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
description?: string | null
|
||||
|
||||
@Property({ columnType: "boolean", default: false })
|
||||
is_giftcard!: boolean
|
||||
|
||||
@Enum(() => ProductTypes.ProductStatus)
|
||||
status!: ProductTypes.ProductStatus
|
||||
|
||||
// TODO: add images model
|
||||
// images: Image[]
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
thumbnail?: string | null
|
||||
|
||||
@OneToMany(() => ProductOption, (o) => o.product)
|
||||
options = new Collection<ProductOption>(this)
|
||||
|
||||
@OneToMany(() => ProductVariant, (variant) => variant.product)
|
||||
variants = new Collection<ProductVariant>(this)
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
weight?: number | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
length?: number | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
height?: number | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
width?: number | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
origin_country?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
hs_code?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
mid_code?: string | null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
material?: string | null
|
||||
|
||||
@ManyToOne(() => ProductCollection, { nullable: true })
|
||||
collection!: ProductCollection
|
||||
|
||||
@ManyToOne(() => ProductType, {
|
||||
nullable: true,
|
||||
index: "IDX_product_type_id",
|
||||
})
|
||||
type!: ProductType
|
||||
|
||||
@ManyToMany(() => ProductTag, "products", {
|
||||
owner: true,
|
||||
pivotTable: "product_tags",
|
||||
index: "IDX_product_tag_id",
|
||||
})
|
||||
tags = new Collection<ProductTag>(this)
|
||||
|
||||
@ManyToMany(() => ProductCategory, "products", {
|
||||
owner: true,
|
||||
pivotTable: "product_category_product",
|
||||
})
|
||||
categories = new Collection<ProductCategory>(this)
|
||||
|
||||
@Property({ columnType: "boolean", default: true })
|
||||
discountable: boolean
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
external_id?: string | null
|
||||
|
||||
@Property({ onCreate: () => new Date(), columnType: "timestamptz" })
|
||||
created_at: Date
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@BeforeCreate()
|
||||
beforeCreate() {
|
||||
this.id = generateEntityId(this.id, "prod")
|
||||
if (!this.handle) {
|
||||
this.handle = kebabCase(this.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Product
|
||||
Reference in New Issue
Block a user